つばくろぐ @takamii228

知は力なり

iOSのPush通知でAPNsとの連携を証明書と認証キーでそれぞれやってみた

iOSアプリのPush通知を実現するには、APNs(Apple Push Notification Service)と連携する必要があります。この連携方式について調べてみたので備忘録としてまとめておきます。

APNsとの連携方式

APNsとの連携はPush通知を受け取るアプリケーションとの連携と、アプリケーションにPush通知を送るようにAPNsへ依頼するアプリケーションとの連携 の2種類行う必要があります。

https://docs-assets.developer.apple.com/published/1fe29f6177/4ebaf4d8-031d-4eb5-b975-07373dfa6eb6.png

Apple Developer Documentation

前者のPush通知を受け取る側の設定についてはiOSのアプリケーションのApp IDをApple Developer Console上で発行するときにPush Notificationsを有効にし、APNsの証明書と紐付けることで実現できます。証明書の発行の手順はやや複雑ですが、ググればいろんな記事がヒットします。

qiita.com

一方で後者のPush通知を発行する側の連携方式については、実際はAPNsと直接やりとりするケースはあまりなく、Firebase Cloud MessagingやニフティクラウドなどMBaaS製品を経由することが多いと思います。今回はこちらの連携方式について詳しく見ていきます。

Push通知をAPNsへ依頼するアプリケーションとの連携方式

Push通知をAPNsへ依頼するアプリケーションとの連携方式は、調べてみると現在は認証キー(p8ファイル)での認証方式と証明書(p12ファイル)での認証方式の2種類があるようです。

認証キーでの認証方式の方が新しく、証明書での認証方式は以前からある方式のようです。それぞれのメリット・デメリットを比較すると以下のようになります。

連携方式 メリット デメリット
認証キー(p8ファイル)での認証 キーの有効期限がなく更新の運用がいらない Apple Developer Programに対して2つまでしか認証キーが発行できない
・App ID単位で管理ができない
・開発環境・本番環境と区別ができない
証明書(p12ファイル)での認証 ・App ID単位で管理ができる
・開発環境・本番環境と区別ができる
1年単位で更新が必要となる

メリット・デメリットはそれぞれの裏返しになっています。

認証キーの方が便利そうに見えますが、キーが2つしか発行できないので、同一アカウントで複数のアプリを開発・配信している場合は鍵の原本管理に注意が必要です。とはいえ証明書のケースでも管理は大変なので、管理対象の数が減って更新運用が不要になるのはだいぶ楽ですね。 今後はどうなっていくかについてはAppleのご都合次第ですが、恐らく認証キーの方に流れていくのではないかと予想しています。

Legacy Binary Protocolでの連携は2021年3月以降サポートされない

ちなみにもっと昔にはLegacy Binary Protocolというものもあったようです。こちらは2021年の3月以降はサポートしないとAppleから発表されています。以前のアナウンスの11月からさらに延長されました。

少し混乱しますが、ここでサポートが切れるのはLegacy Binary Protocolの連携方式であって、証明書ベースの認証方式自体は2021年3月以降も利用できると考えられます(証明書でもhttp2で通信できるますし)(でも違ったらごめんなさい...)。

各種MBaaSにおけるAPNsとの連携方式

ここで各種MBaaSのツールのAPNsとの認証方式が現在どうなっているか見てみましょう。

Firebase

Firebaseの設定手順を見ると、authentication keyとあるので認証キーでの設定が推奨されているようです。

firebase.google.com

FCMの設定画面ではAuthentication Keyが推奨と出ていますが、APNs CertificatesのアップロードUIも用意されているので今現在は両方利用できるようです。

f:id:takamii228:20200713164341p:plain

ニフティクラウド

ニフティクラウドはp12ファイルのアップロードとあるので証明書での認証方式のようです。

mbaas.nifcloud.com

f:id:takamii228:20200713165100p:plain

AWS Amplify

AWSのBaaSツールであるAmplifyはp12ファイルのアップロードとあるので証明書での認証方式のようです。

docs.amplify.aws

Salesforce

セールスフォースはp12ファイルの利用をやめてp8ファイルへの以降に舵を切ったようです。

org62.my.salesforce.com

証明書での認証と認証キーでの認証は共存できる?

証明書での連携方式を今後どこかのタイミングで認証キーへ移行していくケースを考えてみます。証明書(p12ファイル)と認証キー(p8ファイル)は、いずれもPush通知の発行を依頼するサービスがAPNsへリクエストを投げる時の認証に使うものです。そのため、基本的にはAPNsの受け口が空いている限り共存できるのでは思いましたが、少し不安になったので同一アプリに対してp12ファイルで認証したPushのリクエストとp8ファイルで認証したPushのリクエストがいずれも受信できるか実際に検証してみました。

証明書一式を準備する

検証用のアプリを作るにあたっての一式をApple Developer Program上で準備します。

  • 開発用証明書
  • AppID(Push Notificationsをオンにする)
  • provision profile(検証につかう端末を含む)
  • APNs証明書(P12ファイル)
  • AuthKey(p8ファイル)

iOSのサンプルプロジェクトを作る

次にPush通知を受け取るサンプルアプリを作ります。今回はPush通知が受け取れればいいので、デフォルトのSingle Page Viewアプリプロジェクトを作り、AppDelegate.swiftに最低限の設定のみを記述します。PushのCapabilitiesをオンにするのを忘れないようにします。

import UIKit
import UserNotifications

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Push通知の許可ダイアログを出す
        UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
            guard granted else { return }
            DispatchQueue.main.async {
                UIApplication.shared.registerForRemoteNotifications()
            }
        }
        UNUserNotificationCenter.current().delegate = self
        
        return true
    }
   
   ...
    // Push通知の用のデバイストークンをログに吐き出す
    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        let token = deviceToken.map { String(format: "%.2hhx", $0) }.joined()
        print("Device token: \(token)")
    }
    
    // Push通知のペイロードの受信を確認する
    func application(_ application: UIApplication, didReceiveRemoteNotification payload: [AnyHashable: Any]) {
        print(payload)
    }
            
    // フォワグラウンドで通知バナーを出す
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
         completionHandler([.alert, .sound, .badge])
    }
}

この状態でアプリを実機でビルドし、Push通知の許可ダイアログを許可するとログからデバイストークンが取得できます。このデバイストークンを使ってPush通知のリクエストを組みます。

証明書を使ってAPNsへPush通知のリクエストを投げる

p12ファイルを使ったAPNsへのリクエストはcurlコマンドを使って組むことができます。ググってみたらちょうどcurlコマンドがあったのでこれを参考にして組んでみます。

https://gist.github.com/greencoder/16d1f8d7b0fed5b49cf64312ce2b72cc

#!/bin/sh

set -x

P12_CERT_FILE_PATH="xxxxx.p12"
APP_BUNDLE_INDENTIFER="com.takamiii.push-test"
DEVICE_TOKEN="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
API_ENDPOINT="https://api.development.push.apple.com/3/device/"

curl -v \
-d '{"aps":{"alert":{"title":"Test","body":"Hello from request with P12 certificates"}}}' \
-H "Content-Type: application/json" \
-H "apns-topic: ${APP_BUNDLE_INDENTIFER}" \
-H "apns-priority: 10" \
--http2 \
--cert-type P12 --cert ${P12_CERT_FILE_PATH} \
${API_ENDPOINT}${DEVICE_TOKEN}

curlコマンドを実行すると無事200で成功し、Push通知を受信できました。ログからPushが受け取れていることがわかります。

< HTTP/2 200 
< apns-id: 66AF09FA-154B-F68F-CAB3-5CBD49104122
< 
* Connection #0 to host api.development.push.apple.com left intact
* Closing connection 0
[AnyHashable("aps"): {
    alert =     {
        body = "Hello from request with P12 certificates";
        title = Test;
    };
}]

通知バナーも出ています。

f:id:takamii228:20200713174627p:plain

認証キーを使ってAPNsへPush通知のリクエストを投げる

p12ファイルの設定はそのままに、p8ファイルの認証キーを使ったPush通知のリクエストを投げてみます。こちらはjwtの文字列を組む必要があって少し複雑なのですが、こちらのqiita記事を参考を参考にリクエストを組んでみます。

qiita.com

#!/bin/bash

set -x

AUTH_KEY_FILE_PATH="AuthKey_xxxxxx.p8"
AUTH_KEY_ID="xxxxxxxx"
TEAM_ID="xxxxxxxxx"

APP_BUNDLE_INDENTIFER="com.takamiii.push-test"
DEVICE_TOKEN="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
API_ENDPOINT="https://api.development.push.apple.com/3/device/"

base64() {
   openssl base64 -e -A | tr -- '+/' '-_' | tr -d =
}

sign() {
   printf "$1"| openssl dgst -binary -sha256 -sign "${AUTH_KEY_FILE_PATH}" | base64
}

TIME=$(date +%s)
HEADER=$(printf '{ "alg": "ES256", "kid": "%s" }' "${AUTH_KEY_ID}" | base64)
CLAIMS=$(printf '{ "iss": "%s", "iat": %d }' "${TEAM_ID}" "${TIME}" | base64)
JWT="${HEADER}.${CLAIMS}.$(sign ${HEADER}.${CLAIMS})"

curl -v \
   -d '{"aps":{"alert":{"title":"Test","body":"Hello from request with P8 certificates"}}}' \
   -H "Content-Type: application/json" \
   -H "Authorization: Bearer ${JWT}" \
   -H "apns-topic: ${APP_BUNDLE_INDENTIFER}" \
   --http2 \
   ${API_ENDPOINT}${DEVICE_TOKEN}

こちらも無事200で成功し、Push通知を受信できました。ログからPushが受け取れていることがわかります。

< HTTP/2 200 
< apns-id: 09393B36-E1CE-4F9A-5A29-2FC7FF73D424
< 
* Connection #0 to host api.development.push.apple.com left intact
* Closing connection 0
[AnyHashable("aps"): {
    alert =     {
        body = "Hello from request with P8 certificates";
        title = Test;
    };
}]

通知バナーも出ています。

f:id:takamii228:20200713175350p:plain

以上から、p12ファイルとp8ファイルを使ったリクエストは併用できているので、証明書と認証キーの設定は共存できていると考えられます。

なのでもともとニフティクラウドでp12ファイルでPushを送っていたところからp8ファイルを使ったFirebaseでのPushに移行する、なんてことは問題なくできそうということですね。

まとめ

iOSのPush通知についてAPNsとの連携方式ついて調べた結果をまとめてみました。Push通知の実現にはアプリの実装に加えて証明書連携が必要で、その手順は複雑ですが一つ一つ紐解いていけば理解できるものだと思いました。

またAPNsの証明書と認証キーの移行については今のところどちらでも利用できているので、Pushを実際に発行するアプリケーションの連携方式に合わせて利用していけばよいでしょう。認証キーでの連携は今後は増えていくと思われるので、鍵ファイルの管理をきちんとしていく必要があると感じました。

おまけ

Push通知のリクエストのエンドポイントを調査している中でapi.sandbox.push.apple.comapi.development.push.apple.comの2つがあって混乱したので、digで掘ってみました。

$ dig api.sandbox.push.apple.com

; <<>> DiG 9.10.6 <<>> api.sandbox.push.apple.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 32012
;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;api.sandbox.push.apple.com.  IN  A

;; ANSWER SECTION:
api.sandbox.push.apple.com. 269   IN  CNAME   api.sandbox.push-apple.com.akadns.net.
api.sandbox.push-apple.com.akadns.net. 185 IN A   17.188.138.73

;; Query time: 14 msec
;; SERVER: 2404:1a8:7f01:b::3#53(2404:1a8:7f01:b::3)
;; WHEN: Mon Jul 13 18:40:18 JST 2020
;; MSG SIZE  rcvd: 122
$ dig api.development.push.apple.com

; <<>> DiG 9.10.6 <<>> api.development.push.apple.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 45266
;; flags: qr rd ra; QUERY: 1, ANSWER: 5, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;api.development.push.apple.com.  IN  A

;; ANSWER SECTION:
api.development.push.apple.com.   242 IN  CNAME   api.sandbox.push-apple.com.akadns.net.
api.sandbox.push-apple.com.akadns.net. 554 IN A   17.188.165.218
api.sandbox.push-apple.com.akadns.net. 554 IN A   17.188.166.29
api.sandbox.push-apple.com.akadns.net. 554 IN A   17.188.165.219
api.sandbox.push-apple.com.akadns.net. 554 IN A   17.188.166.27

;; Query time: 17 msec
;; SERVER: 2404:1a8:7f01:b::3#53(2404:1a8:7f01:b::3)
;; WHEN: Mon Jul 13 18:40:36 JST 2020
;; MSG SIZE  rcvd: 174

どっちも最終的には同じapi.sandbox.push-apple.com.akadns.netを向いてるようなので、どちらでもよさそうです。

アプリケーションが依存するFWやライブラリからアップデート戦略を考える

最近はFlutterでiOS / Androidアプリ開発をしているのですが、ある程度の頻度でプラットフォームやライブラリのアップデートに追従していかないとあっという間に取り残されてしまう空気を感じています。

ネイティブのOSでいうとちょうど先日Android 11のBetaがリリースされたり、来週WWDCiOS 14が発表されたりと、年に一回は必ずアップデートがある領域ではあるのですが、Flutterはそれ以上に短いサイクルでアップデートが入っています。

Flutter自体はstableのリリース頻度を見ても3-4ヶ月に一回のペースで更新があるようです。

flutter.dev

このようにあるFWを使ってアプリを作ってサービスを継続的に提供していく場合、そのFWのバージョンアップにどうやって追従していくべきかなんとなく考えてみたのでまとめてみます。

追従していくおおまかな流れ

だいたいこんな感じで考えればよいのではないか、というのを以下にまとめてみました。

  1. アプリケーションが依存するものを洗い出す
  2. 依存するものの更新ライフサイクルを確認する
  3. FW/ライブラリのバージョンアップ or リプレースを計画する

今回はFlutterを例に順番に考えてみます。

1.アプリケーションが依存するものを洗い出す

まずアプリケーションが構成しているものを順番に洗い出して、それを要素分解して依存関係を明確にしていきます。

洗い出す観点は以下のとおりです。

  • プログラム・設定ファイル
  • SDK・ライブラリ
  • 実行環境
  • 開発環境

Flutterの場合はまず大きく①FlutterのDart部分、②Androidのネイティブ部分、③iOSのネイティブ部分に分けられます。

f:id:takamii228:20200621204050p:plain

①FlutterのDart部分

FlutterのDart部分はFlutterのSDKに含まれるPackagesを参照する部分と、Pluginをimportする部分があります。

Flutter Pluginは参照するときはpubspec.yamlに記述するだけですが、中身は入れ子でFlutterプロジェクトになっていてDartソースはもちろんAndrodやiOSのネイティブコードを含んでいるものがあるので注意が必要です。

開発にはAndroid StudioAndroid StudioのFlutter Pluginを使います(VS CodeIntelliJのケースもあるでしょう)。

f:id:takamii228:20200621204244p:plain

Androidのネイティブ部分

Androidのネイティブ部分はKotlin / JavaのコードやJAR / AARライブラリ、Android SDKのライブラリやGraldeプラグインからなります。ビルドにはGradleが利用され、build.graldeに設定が記述されます。そもそもAndroidにはJDKが必要ですね。

f:id:takamii228:20200621204548p:plain

iOSのネイティブ部分

iOSのネイティブ部分はSwfit / Objective-Cと、CocoaPodsからimportするライブラリ、XCodeのFrameworkなどからなります。ビルドはXCodeに内包されるSDKで実行されます。

f:id:takamii228:20200621204755p:plain

全体像

①ー③を1枚絵にすると以下のようになります。

f:id:takamii228:20200621204848p:plain

依存するもののアップデート頻度を確認する

洗い出したコンポーネントをカテゴリ別に分類し、更新頻度や依存するものをまとめておきましょう。

項目名 カテゴリ 依存定義場所 更新頻度 依存するもの
XCode 開発環境 開発マシン上 メジャーバージョンは年1回
マイナーバージョンは3-4ヶ月に1回
Mac OS
Android Studio 開発環境 開発マシン上 3-4ヶ月に1回 -
Flutter SDK 開発FW 開発マシン上
pubspec.yaml
3-4ヶ月に1回 Android Studio
XCode
CocoaPods
Android SDK 開発FW build.gralde
AndroidManifest.xml
年に1回 Android Studio
Gradle
iOS SDK 開発FW XCodeプロジェクト設定 3-4ヶ月に1回 XCode
Flutterプラグイン 外部ライブラリ pubspec.yaml ライブラリに依存 Android SDK
iOS SDK
Flutter SDK
Android外部ライブラリ 外部ライブラリ build.gradle ライブラリに依存 Android SDK
Gradle
依存ライブラリ
iOS外部ライブラリ 外部ライブラリ Podfile ライブラリに依存 iOS SDK
CocoaPods
依存ライブラリ
Dart Programs ソースコード - SDKに依存 Flutter SDK
依存ライブラリ
Kotlin Programs
(Java)
ソースコード - SDKに依存 Android SDK
Kotlin Version
JDK
Swift Programs
(Objective-C)
ソースコード - SDKに依存 iOS SDK
Swift Version
XCode

Flutterの場合はFlutterプラグインの中で依存関係があったり、更にAndroidiOSのソースが入っていてSDKを参照してたりするのでより注意が必要です。

FWのバージョンアップ、ライブラリのバージョンアップ or リプレースを計画する

上記の表を並べて見てみると大きなアップデートサイクルとして1年単位のOSメジャーバージョンアップとFWのバージョンアップが4半期に一回程度あるので、少なくともこれらを考慮してバージョンアップを計画しておけばよさそうです。

細かい外部ライブラリのパッチやバグ改修には、もしかしたらFWのアップデートが必要かもしれません。また外部ライブラリのみのパッチであれば月次単位でのアップデートでもよさそうです。

OSのメジャーバージョンアップ対応やフレームワークのバージョンアップは影響が広範囲に及ぶため、マイナーバージョンアップと比べて修正箇所が多くなる可能性があるため作業量を多めに見積もっておきましょう。

いずれにしても、どいういう時にどのバージョンアップを計画しておくのか、事前に決めておくことが大事です。決して据え置きにして蓋をすることはやめましょう。あとになって必ず負債化することになります。

アップデートを安全に行うために

更新のバッチサイズを小さくする

フレームワークのバージョンアップを控えていると、それに合わせてライブラリもバージョンアップしておけばいいや、となりがちです。

しかし、アプリケーションのバージョンアップは広範囲であればあるほど影響範囲が広くなり、変更箇所が多くなって対処に時間がかかります。

なのでバージョンアップの回数分コストが発生するためこまめに複数回やるか、一括で一気にやるかとのトレードオフにはなりますが、影響範囲が狭い範囲でバッチサイズを小さくした細かいバージョンアップを定期的に行っておくほうがリスクを低く抑えられるでしょう。

E2Eテストを用意する

FWをバージョンアップするときに、アプリケーションの振る舞いが変わっていないかをFWのDiffから追うことは結構大変です。

FlutterのRelease Noteを見ると、破壊的な変更は説明があるものの、細かいPRを一つ一つ追っていくのはかなり大変です。

flutter.dev

このとき、アプリケーションの外から見た振る舞いをEnd-to-Endテストで記述して自動で実行できるようにしておくと、バージョンアップで壊れている箇所がないかをすぐに確認することができます。また失敗するテストから修正が必要な箇所をすぐに特定できるようにもなります。ただし、重厚に全機能を網羅するテストを用意するのはメンテナンスコストとのトレードオフにもなるため、まずは正常系の画面遷移のパスを網羅する程度のものを準備しておくとよいでしょう。Flutterの場合はFluter drive testでE2Eテストが記述できます。

アップデートに追従しにくいライブラリはリプレースできないか考える

FWの頻繁な更新に対応しようとすると、場合によっては3rdパーティによるメンテナンスが追いつかないケースもあります。そういう場合はFWの更新と天秤にかけた意思決定が必要です。場合によっては違うライブラリに置き換えたり、自前で作成してしまうなどして依存関係を見直すようにしましょう。

プラグインの選定方法に関しては以前記事をまとめたのでこちらも参考にしてみてください。

takamii.hatenablog.com

この段階でいろいろ考えてみてどうしても負債が解消できないとなった場合は、FWを再選定したりアプリケーションを大々的に作り変えることも選択肢の一つになるかもしれません。

アプリケーションの外の依存関係も明らかにしておく

今回はアプリケーション開発・更新にフォーカスしていますが、サービス提供という一つ上の目線で見てみると、その周りにあるものも依存するものとして捉えることができます。単純にヒト・モノ・カネの観点でいうと、関わっているエンジニアや委託している外部ベンダ、マシンや計算リソース、予算管理などが該当します。また社内の運用ルールなども含めて、起こりうる変更に備えておけばよりアジリティ高く変化に対応できるようにできそうです。

まとめ

プラットフォームやフレームワークを選定し外部に公開されているライブラリと組み合わせてアプリケーションを構成するときは、上記のようなアップデートライフサイクルを考慮した開発方針を事前に決めて追加開発の中で計画的に更新していくことで、技術的な負債の発生を未然に抑止し、アプリケーションを継続的に安定稼働させることができるようになるでしょう。初期リリース偏重で、アップデート戦略なしに小手先の末端のソースコードを継ぎ足すだけの機能開発を続けていると、あっという間にコアな部分で負債が発生する未来がやってきてしまいます。

一昔前のSpringやAngularJS、LaravelのLTS版を見てみると数年単位で塩漬けにしていても安泰ではありましたが、今はもうそのような世界はありません。Javaのバージョンアップライフサイクルに代表されるように、ライブラリやFWの寿命が昔に比べて短命になっていると感じます。

このように作って終わり、あとは塩漬けの時代は終わりつつあるので、継続的に依存ライブラリやFWをどう更新してくか、アプリケーション開発の初期段階できちっと考えておきたいものです。

APKファイルとIPAファイルのバージョン情報をコマンドラインから確認する

apkファイルとipaファイルのバージョン情報をコマンドラインで確認する方法を備忘録として残しておきます。

apkとipaのバージョン情報について

apkファイル及びipaファイルのバージョン情報は、ストア申請のときのバージョニング情報として利用されるものです。それぞれ公式に詳細が記載されています。

両OSともにx.y.zというバージョンの文字列と、バージョンの一意性を表す数字が定義されています。微妙に呼び名が異なるので注意が必要です。

OS バージョン名 ビルド番号
Android versionName versionCode
iOS version number build string

versionNameとversin numberがストアで表示されるときのバージョン番号ですね。

apkのversionNameとversionCodeをコマンドラインから確認する

apkのバージョン番号とビルド番号を確認するのは、Android Studioでapk analyzerにかければすぐにわかりますが、ここではコマンドラインを使う方法を紹介します。

コマンドとしてはAndroid SDKに含まれるaapt2コマンドを使うことでAndroidManifest.xmlの情報が取得できます。それをgrepして加工してあげればほしい情報が出力できます。

developer.android.com

$ aapt2 dump badging sample.apk | grep "versionName" | sed -e "s/.*versionName='//" -e "s/' .*//"
1.2.3
$ aapt2 dump badging sample.apk | grep "versionCode" | sed -e "s/.*versionCode='//" -e "s/' .*//"
100

ipaのversion numberとbuild stringをコマンドラインから確認する

ipaについてはBitriseが公開しているipa_analyzerを使うのが便利です。

github.com

ipaのバージョン情報・ビルド番号はipaが持つinfo.plistの中のCFBundleShortVersionStringおよびCFBundleVersionに記載されています。

ipa_analyzerを使うとinfo.plistの値をjson形式で情報を出力してくれるので、jqコマンドでフィルターをかければよいでしょう。

$ ipa_analyzer -i sample.ipa -p --info-plist --prov | jq -r '.info_plist | .content | .CFBundleShortVersionString'
1.2.3
$ ipa_analyzer -i sample.ipa -p --info-plist --prov | jq -r '.info_plist | .content | .CFBundleVersion'
100

CIマシンでバージョニングを自動化するときに、正しくバージョニングできているかを上記のコマンドを使って確認するとよいでしょう。