ホームページ>開発ツール>Xojo / Real Studio Trial and Error・コマンドラインでNotary Serviceをダイレクトに操作する
Xojo / Real Studio Trial and Error
目次
コマンドラインでNotary Serviceをダイレクトに操作する
はじめに
以下は、Xojo Cocoaビルドについての話題です。
標準的なNotarizationの手法が使えない時の、代替手法を調べてみました。
なお検証には、Xojo 2022 Release 4.1を用いています。(Mac mini 2018 + macOS 13.4.1 Ventura)
経緯
macOSアプリの公証は、Xojo製のようにXcodeで直接行えない場合は、従来はaltoolで行ってきましたが、notarytoolに変更になりました。
altoolは2023年11月以降は使えなくなってしまうので、notarytoolに乗り換える必要があります。
参考サイト(1):Customizing the notarization workflow | Apple Developer Documentation
当方でも発表直後から試行してきましたが、アップロードの途中、なぜか3MBを超えるあたりで常にエラーとなってしまいます。
(注:3MB以内のファイルであれば、正常に処理されます。)
メッセージの様子からは、応答がなくなってタイムアウトした、ような印象です。
Error: abortedUpload(resumeRequest: SotoS3.S3.ResumeMultipartUploadRequest(uploadRequest: SotoS3.S3.CreateMultipartUploadRequest(acl: nil, bucket: "notary-submissions-prod", bucketKeyEnabled: nil, cacheControl: nil, contentDisposition: nil, contentEncoding: nil, contentLanguage: nil, contentType: nil, expectedBucketOwner: nil, _expires: SotoCore.OptionalCustomCoding
(value: nil), grantFullControl: nil, grantRead: nil, grantReadACP: nil, grantWriteACP: nil, key: "prod/AROARQRX7CZS3PRF6ZA5L:fa35c1d2-a487-4408-bc51-6c8b69d082e3", metadata: nil, objectLockLegalHoldStatus: nil, objectLockMode: nil, _objectLockRetainUntilDate: SotoCore.OptionalCustomCoding (value: nil), requestPayer: nil, serverSideEncryption: nil, sSECustomerAlgorithm: nil, sSECustomerKey: nil, sSECustomerKeyMD5: nil, sSEKMSEncryptionContext: nil, sSEKMSKeyId: nil, storageClass: nil, tagging: nil, websiteRedirectLocation: nil), uploadId: "ngPRrcW5MM5f51_ypYU7EACjMTr1GuzIUL2TMWntqZEUhEO.zyqZPcrqJindTf3fpvnBrRgowTkcVMMNXQQNNS8CQ8zZXlOEQLKAmM3GJ4yBckqbF.Hj5Bq5grPSMNkeLxEoUTaZzsPnj3NERXgiXy8MWyR0FumSL3em_Qf8kaoLc9DzvF9_GzGIXgHalsqB", completedParts: []), error: HTTPClientError.deadlineExceeded) ちなみに、上記メッセージ(の一部)でググっても、何もヒットしない。なので、仕方なくaltoolを使い続けていたのですが、そろそろそうも言っていられなくなってきました。
ということは、当方の通信?環境が極めて特殊?で、タイムアウトする人なんて他にはいない、ということか?
だとすれば、ここに書いたことは誰も読まないことになるが、まぁいいか。
そこで調べていたら、以下がヒットしました。
この方法でも同様にエラーで終わる可能性も考えられますが、他に選択肢もなさそうだったので、ひとまず試してみることにしました。
参考サイト(2):Submitting software for notarization over the web | Apple Developer Documentation
読むと、大まかな手順としては、こんな感じです。
1. Create a Private Key
2. Generating Token
3. Submission & Upload
4. Check the Status ( and Get Log )
それぞれ、具体的に見ていきます。
1. Create a Private Key
App Store Connectにログインして、Private Key(サイト上ではAPIキーと表記)を取得します。参考サイト(3)の手順通りにすれば取得できます。
この時、APIキーだけでなく、Issuer IDとキーIDも必要になるので、控えておきます。
参考サイト(3):Creating API Keys for App Store Connect API | Apple Developer Documentation
2. Generating Token
トークンを生成します。参考サイト(4)に手順が書かれています。
参考サイト(4):Generating Tokens for API Requests | Apple Developer Documentation
アップル自身はトークン生成機能を提供していないので、外部に頼ることになりますが、手っ取り早いのはjwt.ioのDebuggerを使わせて頂くことでしょう。
必要になるのは、Header、Payload、Private Key、Public Keyになります。
このうち、Header、Payloadは参考サイト(4)に書かれている通りに作成します。
Public Keyは、Private Keyから抽出します。が、ここで問題が。
jwt.ioのDebuggerに上記をコピー&ペーストしても、Invalid Signatureになってしまいます。
Debuggerページをよく見ると、Private KeyはPKCSの#8と#1に対応していますが、Public Keyは#1のみとなっています。
手元にあるPrivate Keyは#8なので、抽出したPublic Keyも#8となっているため、これを#1に変換しようとしたのですが、エラーになってしまいました。変換方法は複数あるのでいろいろ試しましたが、いずれもダメでした。
手詰まってしまいましたが、ふと、Private Keyを#8から#1に変換後にPublic Keyを取り出せばいいのでは、と思い、やってみたらうまくいきました。
やり方は、ターミナルで、
openssl pkcs8 -topk8 -inform pem -in AuthKey_XXXXXXXXXX.p8 -outform pem -nocrypt -out privkey.p1
再度、jwt.ioのDebuggerに代入したところ、トークンが表示されました。openssl ec -in privkey.p1 -pubout -out pubkey.p1
jwt.ioのDebugger上でエンコードする場合は、Private Key、Public Keyとも#1でないと、Invalid Signatureになってしまう。3. Submission & Upload
一方、(別の処で作られたものを)デコードする場合は、#8でエンコードされたものは#8のPublic Keyで、Signature Verifiedになる。
この辺よくわからない。(実装上の制約?)
参考サイト(2)の実行例は、Python3で書かれています。
当方には、Python3がインストールされていなかったので、まずはここから始めました。(導入方法は様々なサイトで解説されています。)
本体の他、requests、boto3、も追加でインストールしておきます。
参考サイト(2)ではスクリプトが3箇所に分かれて記述されていますが、これらは一つに纏めて記述できます。
ただし、Config絡みのエラーが出たので、import boto3の後にfrom botocore.config import Configを追加しておきました。
トークンは# Defined elsewhere.と書かれていますが、ここではテストなので、上記トークンをダブルコーテーションで括って直接指定しました。
アップロードするファイル名(3箇所)も書き換えておきます。
なお、最初のスクリプトの、bodyのnotificationsは不要だったので削除しておきました。(前行最後のカンマ削除を忘れずに)
サーバーからのレスポンス(参考サイト(2)では赤字で示されているもの)のうち、IDはステータスの確認等に必要となるので書き出すようにしておきます。
output = resp.json()の後に以下を追加しました。
idnumber = output["data"]["id"] # get ID print(idnumber) # output ID
これで、必要な環境は揃いました。あとは上記Pythonスクリプトを実行するだけです。
肝心の3MB超アップロードですが、、、正常にできました!
お陰で、11月以降もNotalizeなしで公開せずに済みそうです。
4. Check the Status ( and Get Log )
参考サイト(2)のcurlコマンドで確認しますが、<token>は作成したトークン、最後の2efe2717-52ef-43a5-96dc-0797e4ca1041は3.のPythonスクリプト実行中に書き出されたIDに置き換えます。
ここまでで、ルートの確認はできましたが、トークンの作成をウェブ上で手作業で行うステップを含む方法は使い勝手がいいとは言えないので、コード化できないか探ってみました。
・jwt.ioでは各言語用にライブラリが用意されているのですが、ドキュメント類が見つからなかったので諦めました。
・Objecive-Cのサンプルソースコードが公開されていて、そのうちのyourkarma / JWTを使わせて頂いたのですが、エラーが発生して、それ以上進めませんでした。
・Opensslのdgstは、処理はできたものの、jwt.ioのDebuggerに代入するとInvalid Signatureに。(ES256に対応していない?)
・PythonライブラリのPyJWTは、以下のサイトを参考にさせて頂いて試したところ、jwt.ioのDebuggerでSignature Verifiedになりました。(注:cryptographyの追加インストールが必要でした。)
参考サイト(5):PyJWTを利用したES256形式のJWT生成方法 メモ - Qiita
ということで、一連の作業をツールとして纏められる目処が立ちました。
さて、上記手作業での確認では、固有の情報をスクリプト等に埋め込んで使いましたが、ツール化するとなると、汎用化は考えておきたいところです。
また、トークンには寿命(10〜20分程度にするのが作法?)を設定しているため、一度作ったものを時刻を気にしながら使い回すより、必要になった時点でその都度新規に作成する方が良さそうです。(トークンとIDは紐付けられていないようで、作り直したトークンでも問題はありませんでした。)
あと、ステータスとログの結果はJSON形式ですが、ベタなテキストで返ってくるので読みづらいため、整形して表示します。
参考サイト(6):Python Tips: JSON を整形して表示したい - Life with Python
以上を踏まえ、(残りの)仕様は以下の通りとしました。
- Pythonスクリプトは、トークン生成、アップロード、JSON文字列整形用、の3つとし、プロジェクトのBuild Settingsで取り込む。
- 各Pythonスクリプトは、パラメーターを外部から指定できるよう、一部改変する。
- コマンドの実行には、Xojo純正のShellを同期モードで使用する。
- Shell動作時のディレクトリ(カレントディレクトリ)は、実行アプリのディレクトリとする。(必要なファイル類は、ここに置く。)
- Shell実行結果がエラーの場合の処理は、特に行わない。(必要なら追加して下さい。)
- Private Key名、Issuer ID、キーIDはコード埋め込みとする。(必要なら外部から取り込めるよう、改変して下さい。)
- Private KeyはPKCS #8、すなわちApp Store Connectからダウンロードしたものをそのまま使う。
- トークン作成時の検証は行わない。(そのため、Public Keyは使わない。)
- 当該アプリのzip圧縮は、事前に手作業で行なっておく。(コマンドを内部で実行する方法もあるが、ここでは行わない。必要なら追加して下さい。)
Pythonスクリプトの編集
jwttoken.py
upload.py
- PyJWTを利用したES256形式のJWT生成方法 メモ - Qiita>実装、のサンプルをベースとする
- 先頭部分に以下を追加
#!/usr/bin/env python3 import sys args = sys.argv fname = args[1] kid1 = args[2] iss1 = args[3] iat1 = args[4] exp1 = args[5]
- 以下を改変
with open('./'+fname) as f_private:
'kid': kid1,
'iss': iss1,
'iat': int(iat1),
'exp': int(exp1),
- 「##### JWT 検証 #####」以下を削除
parse.py
- アップル公式ページのサンプルがベースのため、改変したものを全文載せています。(出典:Submitting software for notarization over the web | Apple Developer Documentation)
#!/usr/bin/env python3 # Phase 0 import sys args = sys.argv token = args[1] fname = args[2] # Phase 1 import hashlib with open(fname, "rb") as file: hash = hashlib.sha256() hash.update(file.read()) sha256 = hash.hexdigest() body = { "submissionName": fname, "sha256": sha256 } # Phase 2 import requests #token = generate_token() # Defined elsewhere. resp = requests.post("https://appstoreconnect.apple.com/notary/v2/submissions", json=body, headers={"Authorization": "Bearer " + token}) resp.raise_for_status() output = resp.json() idnumber = output["data"]["id"] # get ID print(idnumber) # output ID # Phase 3 import boto3 from botocore.config import Config # Added to Original aws_info = output["data"]["attributes"] bucket = aws_info["bucket"] key = aws_info["object"] sub_id = output["data"]["id"] s3 = boto3.client( "s3", aws_access_key_id=aws_info["awsAccessKeyId"], aws_secret_access_key=aws_info["awsSecretAccessKey"], aws_session_token=aws_info["awsSessionToken"], config=Config(s3={"use_accelerate_endpoint": True}) ) resp = s3.upload_file(fname, bucket, key)
- Python Tips: JSON を整形して表示したい - Life with Python>シンタックスハイライトなし>JSON が文字列に格納されている場合、のサンプルをベースとする
- 先頭部分に以下を追加
#!/usr/bin/env python3 import sys args = sys.argv result = args[1]
- JSON_SAMPLEを以下に改変
JSON_SAMPLE = result
Xojoでの実装
注:以下の実装では、一部Xojo 2022r4.1ではDeprecatedな機能が使われています。必要なら推奨される機能に置き換えて下さい。
【ソースコードのコピー&ペーストについて】
・ソースコード(グレー背景部分の全文)をコピーし、指定のオブジェクトにペーストすると、(新規作成して名前等を個別にコピー&ペーストしなくても)復元されます。
・ペーストはオブジェクトに行って下さい。オブジェクト内のEvent Handlers/Methods/Properties等にペーストしても、うまくいかない場合があります。
・それでもペーストできない場合は、各項目のカッコ内を適用して下さい。
実行してみたところ、ファイルがアップロードされ、公証を受けられたことを確認しました。
- Xojoで新規プロジェクトを作成
- Window1に、DesktopButton3個(Name:Button1, Caption:Upload、Name:Button2, Caption:Status、Name:Button3, Caption:Log)、DesktopTextArea4個(Name:TextArea1、Name:TextArea2、Name:TextArea3、Name:TextArea4)、DesktopTextField(Name:TextField1)を置く
- 以下をButton1にペースト(できなければ、Sub - Endの間をPressedイベントに記述)
Sub Pressed() Handles Pressed Upload() End Sub
- 以下をButton2にペースト(できなければ、Sub - Endの間をPressedイベントに記述)
Sub Pressed() Handles Pressed Status() End Sub
- 以下をButton3にペースト(できなければ、Sub - Endの間をPressedイベントに記述)
Sub Pressed() Handles Pressed Log() End Sub
- 以下をWindow1にペースト(できなければ、Sub - Endの間をOpeningイベントに記述)
Sub Opening() Handles Opening // 以下は、各自の環境に合わせる pPrivName="AuthKey_8xxxxxxxx2.p8" pIssuerID="6xxxxxxe-686e-47e3-exxxxxxxxxxxxxxx1" pKeyID="8xxxxxxxx2" pFileName="untitled1.0.0.zip" End Sub
- 以下をWindow1にペースト
Private Function GetEpochTime(sec As Integer) As String Var d1 As DateTime = DateTime.Now Var s1 As Double = d1.SecondsFrom1970 s1=s1+sec Var d2 As New DateTime(s1,TimeZone.Current) Var s2 As Double = d2.SecondsFrom1970 Var s2i As Integer = s2 Var value As Integer = Integer.FromHex(hex(s2i)) return str(value) End Function
- 以下をWindow1にペースト
Private Sub Log() // Log確認用文字列生成 Dim str As String str="curl -H ""Authorization: Bearer "+MakeToken()+""" ""https://appstoreconnect.apple.com/notary/v2/submissions/"+TextField1.Text+"/logs""" // Shell生成 Var s As Shell s = new Shell // Shell実行 s.Execute(str) if s.ExitCode = 0 then TextArea4.Text=ParseJSON(s.Result) else MessageBox("Error code: " + s.ErrorCode.ToString) TextArea4.Text=s.Result end if End Sub
- 以下をWindow1にペースト
Private Function MakeToken() As String Dim path As String = GetFolderItem("").ShellPath // Current Directory Path // Command生成 Dim str, str0, str1, str2, str3, str4, str5, str6, str7 As String str0=" " str1="cd "+path+";/Users/hasu/.pyenv/shims/python " str2=SpecialFolder.Resource("jwttoken.py").ShellPath str3=pPrivName str4=pKeyID str5=pIssuerID str6=GetEpochTime(0) // 現在時刻のUNIX Epoch Time str7=GetEpochTime(20*60) // 現在時刻から20分後のUNIX Epoch Time str=str1+str2+str0+str3+str0+str4+str0+str5+str0+str6+str0+str7 // Shell生成 Var s As Shell s = new Shell // Shell実行 s.Execute(str) if s.ExitCode = 0 then Dim ss As String = NthField(s.Result,EndOfLine.UNIX,1) // 最後に改行が付いて返ってくるので、除去する TextArea1.Text=ss return ss else MessageBox("Error code: " + s.ErrorCode.ToString) return "" end if End Function
- 以下をWindow1にペースト
Private Function ParseJSON(res As String) As String Dim path As String = GetFolderItem("").ShellPath // Current Directory Path // data以降を抽出 Dim res2 As String = NthField(res,"{""data"":",2) res2="'{""data"":"+res2+"'" // Command生成 Dim str, str0, str1, str2, str3 As String str0=" " str1="cd "+path+";/Users/hasu/.pyenv/shims/python " str2=SpecialFolder.Resource("parse.py").ShellPath str3=res2 // 本文 str=str1+str2+str0+str3 // Shell生成 Var s As Shell s = new Shell // Shell実行 s.Execute(str) if s.ExitCode = 0 then else MessageBox("Error code: " + s.ErrorCode.ToString) end if // 整形したテキストを返す return s.Result End Function
- 以下をWindow1にペースト
Private Sub Status() // Status確認用文字列生成 Dim str As String str="curl -H ""Authorization: Bearer "+MakeToken()+""" ""https://appstoreconnect.apple.com/notary/v2/submissions/"+TextField1.Text+"""" // Shell生成 Var s As Shell s = new Shell // Shell実行 s.Execute(str) if s.ExitCode = 0 then TextArea3.Text=ParseJSON(s.Result) else MessageBox("Error code: " + s.ErrorCode.ToString) TextArea3.Text=s.Result end if End Sub
- 以下をWindow1にペースト
Private Sub Upload() Dim path As String = GetFolderItem("").ShellPath // Current Directory Path // Command生成 Dim str, str0, str1, str2, str3, str4 As String str0=" " str1="cd "+path+";/Users/hasu/.pyenv/shims/python " str2=SpecialFolder.Resource("upload.py").ShellPath str3=MakeToken() // トークンはその都度生成 str4=pFileName str=str1+str2+str0+str3+str0+str4 // Shell生成 Var s As Shell s = new Shell // Shell実行 s.Execute(str) if s.ExitCode = 0 then Dim ss As String = NthField(s.Result,EndOfLine.UNIX,1) // 最後に改行が付いて返ってくるので、除去する TextField1.Text=ss else MessageBox("Error code: " + s.ErrorCode.ToString) TextArea2.Text=s.Result end if End Sub
- プロジェクト左ペインのBuild Settings>macOS上で右クリックし、Add to "Build Settings">Build Step>Copy Filesを選択
- InspectorのDestinationにResources Folderを指定
- 中央ペインに、jwttoken.py、parse.py、upload.pyをドラッグ&ドロップ
おわりに
なぜ、notarytoolがダメで、ダイレクトだとうまくいくのかは、結局よく分かりませんでした。
いずれにしても、18MB程度のアップロードに成功しているので、代替手段としては問題なく使えるとみて良さそうです。
ツール化に関しては、使い勝手向上のため、カレントディレクトリを都度変更するか、ファイル類は全てフルパス指定するか、といった改良の余地があります。
アップロードは同期モードのため、実行中はビジー状態になり、IDも終わってから表示されますが、その間やることもないのでそのままにしてあります。経過時間を表示したり、処理を中止する機能を追加したい等の場合は、非同期モードやインタラクティブモードに設定することになります。
また、トークンは使い捨て感覚ですが、Issuer ID、キーID、APIキー(Private Key)は使い回しができるようです。
なお、Stapleのステップはaltoolの時と変わらないため、ここでは省略しています。
お世話になったサイト
貴重な情報をご提供頂いている皆様に、お礼申し上げます。(以下、順不同)
参考サイト(1):Customizing the notarization workflow | Apple Developer Documentation
参考サイト(2):Submitting software for notarization over the web | Apple Developer Documentation
参考サイト(3):Creating API Keys for App Store Connect API | Apple Developer Documentation
参考サイト(4):Generating Tokens for API Requests | Apple Developer Documentation
参考サイト(5):PyJWTを利用したES256形式のJWT生成方法 メモ - Qiita
参考サイト(6):Python Tips: JSON を整形して表示したい - Life with Python
更新履歴
2023.08.28 新規作成
[Home] [MacSoft] [Donation] [History] [Privacy Policy] [Affiliate Policy]