NMOS IS-10の動作検証

NMOS IS-10の動作検証

はじめに

NMOSの認可仕様IS-10を、オープンソースのnmos-cppとKeycloakでAWS上に構築し、トークンが発行されて検証されるまでをパケットレベルで追いかけました。この記事はその記録です。

放送設備のIP化が進むと、映像の切り替えや機器の登録といった制御が、すべてネットワーク越しのHTTP APIで行われるようになります。IS-04とIS-05だけの環境では、ネットワークに到達できれば誰でもRegistryに機器を登録したり、Receiverの接続先を切り替えたりできてしまいます。本番中の映像を誤操作や不正操作から守るために、APIそのものに認可の制御が必要になります。それを担うのがIS-10です。

2025年のInterBEEのIP PavilionではNHKがIS-10のデモを行ったり、リレーセミナーでも東海林氏がNMOS IS-10についてお話しされたりと、業界内でも徐々に注目されている規格です。 (参考 INTER BEE DX x IP PAVILION | InterBEE 2025)

この記事で扱うのは次の内容です。

  • IS-10の仕組み(OAuth 2.0とJWT)の概要
  • nmos-cppとKeycloakによる検証環境の構築ポイント
  • Wiresharkで見た、Nodeの登録からトークン検証までの流れ

なお、この記事の検証環境は簡単のためHTTP(TLSなし)で構築しています。IS-10は本来TLSが前提なので、実際にはさらなる実装が必要となります。

NMOSのおさらい

NMOS(Networked Media Open Specifications)は、AMWA(Advanced Media Workflow Association)が策定している、ネットワーク上のメディア機器を制御・管理するためのオープン仕様群です。SMPTE ST 2110が映像・音声・ANCといったデータの伝送方法を定めているのに対し、NMOSは機器の発見や接続の切り替えといった制御手順、つまりコントロールプレーンを担います。

詳しくは本ブログの過去記事もご覧ください。

今回関係するのは主に次の3つの仕様です。

仕様 名称 役割
IS-04 Discovery & Registration 機器(Node)が自分の情報をRegistryに登録し、コントローラがQuery APIでそれを検索する
IS-05 Device Connection Management コントローラが各機器のConnection APIを呼び、SenderとReceiverの接続を切り替える
IS-10 Authorization 上記APIへのリクエストにアクセストークンを要求し、認可を行う

IS-04で「どんな機器がどこにいるか」が分かり、IS-05で「どの映像をどこに流すか」を切り替えます。IS-10は、これらのAPIを呼ぶ側に認可を与える仕組みです。

IS-10とは

IS-10は、OAuth 2.0とJWT(JSON Web Token)をベースにしたNMOSの認可仕様です。クライアントは認可サーバから取得したアクセストークンを Authorization: Bearer ヘッダに付けてAPIを呼び出し、APIを提供する側はそのトークンを検証してから処理を受け付けます。

登場人物

OAuth 2.0の3つの役割は、NMOSでは次のように対応します。

OAuthの役割 NMOSでの担い手 今回の環境
Authorization Server トークンを発行する Keycloak
Resource Server トークンを検証してAPIを提供する Registry(Registration/Query API)、Node(Connection APIなど)
Client トークンを取得してAPIを呼ぶ Registryに登録しに行くNode、コントローラ

ポイントは、Nodeが2つの役割を兼ねることです。Registryに登録しに行くときはClientとして振る舞い、自分のConnection APIを守るときはResource Serverとして振る舞います。

アクセストークンの中身

IS-10のアクセストークンはJWTで、署名アルゴリズムはRS512(RSASSA-PKCS1-v1_5 + SHA-512)と決められています。ペイロードには、発行者を示す iss、宛先を示す aud、有効期限の exp、許可されたAPI群を示す scope などが入ります。

NMOS独自なのが x-nmos-* というプライベートClaimです。APIごとに、読み取り(read)と書き込み(write)を許可するパスを配列で指定します。

"x-nmos-connection": {
  "read": ["*"],
  "write": ["single/*"]
}

この例では、Connection APIの全パスの読み取りと、single/ 配下への書き込みを許可しています。パスはAPIバージョンより後ろの部分で指定し、* のワイルドカードが使えます。なお、write を与えても read は自動では付きません。読み取りも許可したい場合は、両方を明記する必要があります。

参考Behaviour: Access Tokens | amwa is-10

Resource Serverは、事前に取得しておいた認可サーバの公開鍵で署名を検証します。リクエストのたびに認可サーバへ問い合わせる必要がないため、認可サーバの負荷が下がり、一時的に認可サーバへ到達できなくなっても動作を続けられます。

テスト環境

検証環境は、AWSの同一VPC内に、EC2を3台立てて構築しました。実際にはNodeは放送機器になるかと思います。

役割 ソフトウェア IPアドレス
Registry(RDS) nmos-cpp-registry 172.25.1.10
Node nmos-cpp-node 172.25.1.11
認可サーバ Keycloak(前段にnginx) 172.25.1.49

NMOSの検証環境としてはeasy-nmosが手軽ですが、今回はIS-10の設定を細かく変えたかったため、nmos-cppをソースコードからビルドしました。ビルドすると nmos-cpp-node、nmos-cpp-registry、nmos-cpp-test の3つのバイナリができます。起動時に以下のように設定ファイル(JSON)を引数で渡すと、IS-10の有効・無効や認可サーバのアドレスなどを指定できます。

./nmos-cpp-registry registry_config_is10.json

Node側の設定ファイルのうち、IS-10に関係する主な項目は次のとおりです(イニシャルアクセストークンは省略しています)。

{
  "is10_versions": ["v1.0"],
  "registry_address": "172.25.1.10",
  "registration_port": 3210,
  "client_secure": false,
  "server_secure": false,
  "authorization_address": "172.25.1.49",
  "authorization_port": 80,
  "client_authorization": true,
  "server_authorization": true,
  "authorization_flow": "client_credentials",
  "initial_access_token": "(省略)",
  "authorization_scopes": ["registration", "query", "connection", "events"],
  "token_endpoint_auth_method": "client_secret_basic"
}

client_authorization と server_authorization の両方が true になっているのは、前述のとおりNodeがClientとResource Serverの両方を兼ねるためです。
なお、client_secure と server_secure が false なのは、今回TLSを使っていないためです。

Keycloakとリバースプロキシの設定

Keycloakの設定は、AMWAのAuthorization Server Setup(INFO-002 Security Implementation Guide)を参考にしました。押さえるべき点は3つです。

  • Realm:テナントのような管理単位です。NMOS用のRealm(今回は nmos)を作り、署名アルゴリズムをIS-10の要求に合わせてRS512にします。
  • Client Scope:registration や query など、「どのAPIを使ってよいか」をまとめた許可セットです。プロトコルはopenid-connectにします。
  • Mapper:トークンに埋め込むClaimをカスタマイズします。aud と、x-nmos-registration などのプライベートClaimをここで追加します。

リバースプロキシでメタデータのパスを合わせる

Keycloakをそのまま使うと、IS-10が要求するメタデータURIとKeycloakの実装パスが合わないため、NMOSのクライアントが認可サーバのメタデータを取得できません。

  • IS-10が要求するメタデータURI:/.well-known/oauth-authorization-server
  • Keycloakの実装パス:/realms/<Realm名>/.well-known/openid-configuration

そこでKeycloakの前段にnginxを置き、パスをエイリアスしました。

location = /.well-known/oauth-authorization-server {
    proxy_pass $keycloak_target/realms/nmos/.well-known/openid-configuration;
}

これで http://172.25.1.49/.well-known/oauth-authorization-server へのアクセスが、Keycloakのメタデータに転送されます。本来はこのnginxでTLSも終端するのが筋ですが、今回は省略しています。

今回の検証におけるフロー

RegistryとNodeを起動し、Wiresharkでキャプチャした通信を時系列に並べたのが下の図です。Nodeは最初トークンなしで登録を試みて401で拒否され、そこからクライアント登録とトークン取得を経て、改めて登録に成功しています。

Node (172.25.1.11)Registry (172.25.1.10)Keycloak (172.25.1.49)① メタデータ取得② 公開鍵(JWKS)取得③ メタデータ取得④ 登録(トークンなし)⑤ 401 Unauthorized⑥ クライアント登録(初期トークン)⑦ client_id / client_secret 発行⑧ 公開鍵(JWKS)取得⑨ トークン要求(Basic認証)⑩ アクセストークン(300秒)⑪ 登録(Bearerトークン付き)⑫ 公開鍵で署名を検証⑬ 201 CreatedNode (172.25.1.11)Registry (172.25.1.10)Keycloak (172.25.1.49)

Registryの準備(①②)

Registryは起動直後に、認可サーバのメタデータ(/.well-known/oauth-authorization-server)と公開鍵(jwks_uri)を取得します。公開鍵には "alg": "RS512" と鍵ID(kid)が含まれており、後でトークンを検証する際に使われます。

最初の登録は401で拒否される(③〜⑤)

Nodeもメタデータを取得したあと、まずはトークンを持たないまま /x-nmos/registration/v1.3/resource にPOSTします。ですが、Registryはこれを401 Unauthorizedで拒否します。この401をきっかけに、Nodeは認可の手続きに入ります。

クライアント登録(⑥⑦)

Nodeは、Keycloakの動的クライアント登録エンドポイントに自分を登録します。このとき、設定ファイルに書いたイニシャルアクセストークンをBearerトークンとして付けます。登録が成功すると、201 Createdとともに client_id と client_secret が返されます。

なお、イニシャルアクセストークンの署名アルゴリズムはHS512でした。これはKeycloakがクライアント登録用に発行する別種のトークンで、IS-10がRS512を要求するのはAPIアクセス用のアクセストークンのほうです。

トークンの取得(⑧〜⑩)

Nodeは自分のAPIを守るResource Serverでもあるため、ここで公開鍵を取得しています(⑧)。続いて、Client Credentialsフローでトークンエンドポイントにリクエストします(⑨)。client_id:client_secret をBase64エンコードした値をBasic認証ヘッダに載せる、client_secret_basic 方式です。

レスポンスとして、有効期限300秒のBearerトークンが返ってきました。

{
  "access_token": "(省略)",
  "expires_in": 300,
  "token_type": "Bearer",
  "scope": "connection registration"
}

トークン付きで登録し、検証される(⑪〜⑬)

Nodeは取得したトークンを Authorization: Bearer ヘッダに付けて、再び登録をPOSTします。Registryは手元の公開鍵で署名を検証し、問題がなければ201 Createdを返します。

トークンをJWT Debuggerなどでデコードすると、中身を確認できます。主な値は次のとおりです。

Claim 値 意味
alg(ヘッダ) RS512 IS-10が要求する署名方式
kid(ヘッダ) ②で取得した鍵と同じID どの公開鍵で検証するかを示す
iss http://172.25.1.49/realms/nmos 発行した認可サーバ
aud “*” トークンの宛先(後述)
azp ⑦の client_id と同じ値 トークンを要求したクライアント
iat / exp 差がちょうど300秒 発行時刻と有効期限
x-nmos-registration read / writeとも * Registration APIへの権限

iat をUTCから日本時間に直すと14:39:58で、Wiresharkのキャプチャ時刻と一致しました。公開鍵を渡すと「Signature Verified」と表示され、Registryと同じ検証を手元で再現できます。

Query APIで挙動を確認する

登録が済んだRegistryに、コントローラの立場でQuery APIを叩いてみました。トークンなし、トークンあり、期限切れの3パターンで、レスポンスがどう変わるかを確認します。

トークンなし

$ curl -s http://172.25.1.10:3210/x-nmos/query/v1.3/nodes/ | jq .
{
  "code": 401,
  "error": "missing Authorization header",
  "debug": null
}

Authorizationヘッダがないため、401が返ります。

トークンあり

ユーザー用のトークンをKeycloakから取得します。今回はデモを簡単にするため、ユーザー名とパスワードを直接送る password グラントを使いました。

$ export TOKEN=$(curl -s -X POST "http://172.25.1.49:80/realms/nmos/protocol/openid-connect/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=password" \
  -d "client_id=nmos-test-client" \
  -d "username=(省略)" \
  -d "password=(省略)" \
  -d "scope=query" \
  | jq -r '.access_token')

このトークンを付けると、登録されたNodeやDeviceの一覧が取得できます。

$ curl -s -H "Authorization: Bearer $TOKEN" \
  http://172.25.1.10:3210/x-nmos/query/v1.3/nodes/ | jq .

レスポンスを見ると、Nodeの api.endpoints やDeviceの controls(connection、events、channelmapping、configurationなど)に、すべて "authorization": true が付いています。これはIS-04側のフィールドで、「このエンドポイントを呼ぶにはトークンが必要」という意味です。コントローラはこのフラグを見て、トークンを付けるべきかを判断できます。

期限切れ

今回はトークンの有効期限を300秒にしています。5分待って同じリクエストを送ると、401が返ってきました。

{
  "code": 401,
  "error": "token expired",
  "debug": null
}

IS-10では、トークンがない場合や期限切れの場合は401を、x-nmos-* Claimで許可されていない操作には403を返すと定められていますので、今回の結果は仕様通りでした。

注意点

今回の検証では問題がなかったものの、現実的な実装では注意しないといけない点はいくつかあります。以下はその中でも特に注意したい点です。

aud が配列になっていない

デコードしたトークンの aud は "*" という文字列でした。IS-10では、aud はJSON配列でなければならないと定められています(Access Tokens | is-10)。中身も、宛先の完全なドメイン名か、ワイルドカードを含むドメイン名が推奨されています。

今回の環境はIPアドレス直指定でドメイン名がないため、ドメイン名での照合ができず、* で全宛先を許可しています。本番では、DNSでホスト名を引ける環境を用意し、aud を宛先のドメインに絞るべきですね。

ユーザーのトークン取得は本来Authorization Code

デモではpasswordグラントを使いましたが、BCP-003-02では、機器間通信を行うConfidential Client以外はAuthorization Codeグラントを使うことが想定されています(BCP-003-02)。実際のコントローラでは、ブラウザでログインするフローになります。

まとめと今後

nmos-cppとKeycloakの組み合わせで、IS-10によるNodeの登録とQuery APIの保護が実際に動くことを確認できました。パケットを追うと、トークンなしの登録が401で弾かれ、クライアント登録とトークン取得を経て201で登録が通る流れが見えました。

一方で、実際の運用を考えると、まずはこれをTLS化しないといけません。そうなると放送用のネットワーク内で証明書の運用であったり、そもそもこれらを稼働させるためにサーバの運用や認証・認可の知識を持った放送技術者が必要になります。
どのような運用の中でこの仕組みを実現するのかをまずは放送局側が考えないといけないでしょう。

Previous Post