本記事は生成AIと共同で執筆しています。事実関係は可能な範囲で公式ドキュメント・一次情報と照合していますが、誤りが含まれている可能性があります。重要な判断を行う前にご自身でも一次情報をご確認ください。

デジタルアイデンティティの記事を書いていて、比較対象としてたびたび「学認」と「SAML」が出てくるのに、その中身を説明せずに使っていました。幸い、学認のメタデータは誰でも取得できる形で公開されているので、実際に開いて中を見ます。数値はすべて 2026-08-12 に取得したものです。

この記事は連載の一部で、デジタルアイデンティティとは何かW3C は Verifiable Credentials の何を決めていて、何を決めていないかアイデンティティ・ウォレットの 4 つの機能と、学認のトラストアンカーGaia-X Registry には何が入っているのかと対になります。

SAML とは何をする規格か

SAML (Security Assertion Markup Language) は、ある組織が「この利用者はこういう人です」と述べた文書を、別の組織へ安全に渡すための規格です。OASIS が策定していて、現行の 2.0 は 2005 年に標準化されています。述べられた内容そのものをアサーション (assertion) と呼び、発行元の署名が付きます。

登場人物は 2 者です。属性を出す側を IdP (Identity Provider)、それを受け取ってサービスを提供する側を SP (Service Provider) と呼びます。大学の文脈では、IdP が所属大学の認証基盤、SP が電子ジャーナルや各種の学内外サービスにあたります。実装としては Shibboleth が広く使われています。2000 年に Internet2 のミドルウェア活動として始まり、現在は Jisc・Internet2・SWITCH が 2013 年に設立した Shibboleth Consortium が開発・保守しているオープンソースの実装です。

SAML のログインでメタデータが効いている場所を示した図。上段に利用者 (ブラウザ)、SP (電子ジャーナルなど)、IdP (所属大学の認証基盤) の 3 つが並ぶ。① 利用の矢印が利用者から SP へ、② 転送の矢印が SP から IdP へ、③ 属性つきのアサーションの矢印が IdP から SP へ戻る。SP の下に ④ 属性を見て可否を判断、の箱がある。下段に「事前に配られている名簿 (メタデータ)。SP は IdP の署名鍵を、IdP は SP の受け取り先 URL を、ここから知る」という帯があり、そこから SP と IdP へそれぞれ「署名鍵を知る」「送付先を知る」の矢印が上向きに伸びている。最下部に「名簿が無いと、SP は『そのアサーションが本物の IdP のものか』を判定できず、IdP は『属性を送ってよい相手か』を判定できない。この名簿に学認が 1 本の鍵で署名し、各参加サーバはその署名者証明書を手元に置いて検証する」と記されている。

当日のやりとりは 3 者だけで完結しますが、その前提として、SP は IdP の署名鍵を、IdP は SP の受け取り先 URL を知っている必要があります。この事前共有のための記述がメタデータです。

学認とは何か

学認 (GakuNin、学術認証フェデレーション) は、この IdP と SP の集まりを国内の学術機関の範囲でまとめたもので、国立情報学研究所 (NII) の事業として学術認証運営委員会が運営しています。参加機関が個別に相手を登録するのではなく、参加者全員分の記述を 1 つの XML にまとめ、学認が 1 本の鍵で署名して配信する形をとっています。

curl -s -o gakunin.xml https://metadata.gakunin.nii.ac.jp/gakunin-metadata.xml
wc -c < gakunin.xml
 5655173

約 5.7 MB(5,655,173 バイト)の XML でした。これは運用フェデレーション用のメタデータで、ほかにテストフェデレーション用と、国際的なフェデレーション間接続 (eduGAIN) との相互接続用が別の URL で配信されています。中身を数えます。

python3 -c '
import xml.etree.ElementTree as ET
MD = "{urn:oasis:names:tc:SAML:2.0:metadata}"
r = ET.parse("gakunin.xml").getroot()
ents = r.findall(MD + "EntityDescriptor")
idp = [e for e in ents if e.find(MD + "IDPSSODescriptor") is not None]
sp = [e for e in ents if e.find(MD + "SPSSODescriptor") is not None]
print(r.get("Name"), len(ents), len(idp), len(sp), r.get("validUntil"))
'
GakuNin 650 419 231 2026-08-24T14:00:00Z

650 エンティティで、内訳は IdP が 419、SP が 231 でした。両方を兼ねるエンティティは 0 件です。ルート要素に validUntil が付いていて、この名簿自体に 2026-08-24 という期限があります。参加サーバは期限内に取り直す運用になります。

1 エンティティに何が書かれているか

1 件を開いてみます。

python3 -c '
import xml.etree.ElementTree as ET
MD = "{urn:oasis:names:tc:SAML:2.0:metadata}"
MDUI = "{urn:oasis:names:tc:SAML:metadata:ui}"
r = ET.parse("gakunin.xml").getroot()
for e in r.findall(MD + "EntityDescriptor"):
    d = e.find(MD + "IDPSSODescriptor")
    if d is None or "idp.nii.ac.jp" not in (e.get("entityID") or ""):
        continue
    print(e.get("entityID"))
    print([n.text for n in e.iter(MDUI + "DisplayName")][:2])
    print(d.get("protocolSupportEnumeration"))
    for sso in d.findall(MD + "SingleSignOnService"):
        print(sso.get("Binding").split(":")[-1], sso.get("Location"))
    break
'
https://idp.nii.ac.jp/idp/shibboleth
['国立情報学研究所', 'National Institute of Informatics']
urn:oasis:names:tc:SAML:1.1:protocol urn:mace:shibboleth:1.0 urn:oasis:names:tc:SAML:2.0:protocol
AuthnRequest https://idp.nii.ac.jp/idp/profile/Shibboleth/SSO
HTTP-POST https://idp.nii.ac.jp/idp/profile/SAML2/POST/SSO
HTTP-Redirect https://idp.nii.ac.jp/idp/profile/SAML2/Redirect/SSO

学認のメタデータの 1 エンティティに何が書かれているかを 6 行で示した図。entityID は https://idp.nii.ac.jp/idp/shibboleth で、この IdP を一意に指す名前 (URL 形式だが取得先ではない)。mdui:DisplayName は 国立情報学研究所 / National Institute of Informatics で、選択画面に出る表示名、日英が併記されている。protocolSupport は SAML 1.1 / Shibboleth 1.0 / SAML 2.0 で、話せるプロトコルの世代。KeyDescriptor は X.509 証明書 (この IdP では use 指定のない 1 本で署名・暗号の両方に使う) で、アサーションの署名を検証するための鍵。SingleSignOnService は /idp/profile/SAML2/POST/SSO ほかで、利用者を送り込む先の URL と結合方式。shibmd:Scope は nii.ac.jp で、この IdP が名乗ってよい組織ドメイン (全体で 413 種)。最下部に「650 エンティティ (IdP 419 / SP 231) が、これと同じ形で 1 つの XML に並んでいる」と記されている。

entityID が URL の形をしているのは慣例で、そこにアクセスして何かが取れるとは限りません。あくまで識別子です。ログイン画面で大学を選ぶときに出てくる機関名は mdui:DisplayName から来ていて、日本語と英語が併記されています。

shibmd:Scope は、その IdP が名乗ってよい組織ドメインの宣言です。nii.ac.jp の IdP が u-tokyo.ac.jp の利用者を名乗ることはできない、という制約がここで効きます。全体では 413 種類のドメインが宣言されていました。

SP はどんな属性を要求しているか

SP 側の記述には、そのサービスがどの属性を必要とするかが RequestedAttribute として書かれています。集計してみます。

python3 -c '
import xml.etree.ElementTree as ET, collections
MD = "{urn:oasis:names:tc:SAML:2.0:metadata}"
r = ET.parse("gakunin.xml").getroot()
sps = [e for e in r.findall(MD + "EntityDescriptor") if e.find(MD + "SPSSODescriptor") is not None]
cnt = collections.Counter()
declared = 0
for e in sps:
    ras = [ra.get("FriendlyName") or ra.get("Name") for ra in e.iter(MD + "RequestedAttribute")]
    if ras:
        declared += 1
    cnt.update(ras)
print(len(sps), declared)
for k, v in cnt.most_common(12):
    print(v, k)
'
231 153
106 eduPersonPrincipalName
66 mail
47 organizationName
46 eduPersonTargetedID
37 displayName
30 jaOrganizationName
28 eduPersonAffiliation
23 surname
23 givenName
22 jaDisplayName
21 organizationalUnitName
19 eduPersonScopedAffiliation

231 の SP のうち、要求属性を明記しているのは 153 でした。最も多いのが eduPersonPrincipalName で 106 件です。これは記述の延べ数で、同じ SP が複数の AttributeConsumingService で重複して宣言している場合があるため、ユニークな SP 数はこれより少なくなります(この属性では 105)。なお属性の Name には urn:oid: 形式のほかに旧来の urn:mace:dir:attribute-def: 形式が 20 件混在していて、上の集計は FriendlyName で数えているため両方を含みます。

属性名は eduPerson という、教育機関向けに定義されたスキーマから来ています。Internet2 の MACE-Dir ワーキンググループが作り、現在は REFEDS が保守しています。よく出てくるものだけ挙げます。

属性意味
eduPersonPrincipalNameスコープ(@ の右側の管理ドメイン)内で一意な利用者識別子。休眠後の再割り当てが仕様上許されているため、恒久的に同一人物を指す保証はない12345@u-tokyo.ac.jp
eduPersonTargetedIDSP ごとに値を変えて名寄せを困難にする識別子(非相関は保証されない)。eduPerson 202001 (v4.2.0) で非推奨となり、OASIS の urn:oasis:names:tc:SAML:attribute:pairwise-id が後継SP ごとに別の不透明な値
eduPersonAffiliation役割。統制語彙は 8 値 (faculty student staff alum member affiliate employee library-walk-in)faculty / student / staff
eduPersonScopedAffiliation役割と所属ドメインの組faculty@u-tokyo.ac.jp
eduPersonEntitlement特定のリソースに対する権利の集合を示す URI (URN または URL)電子ジャーナルの利用権など
jaOrganizationName日本語の機関名日本語圏向けに追加された属性

ja から始まる属性は日本語表記のために学認側で定義されたもので、urn:oid:1.3.6.1.4.1.32264.1.1.x という独自の OID (Object Identifier) が振られています。32264 は NII に割り当てられた IANA の Private Enterprise Number で、同じ枝には ja で始まらない学認独自の属性も含まれています。国際的な eduPerson の枠にそのまま収まらない部分を、フェデレーション側で拡張していることが読み取れます。

「所属を証明する」用途では eduPersonScopedAffiliation が最も直接的ですが、実際に要求している SP は 16 件(記述の延べ数では 19 件。うち 4 件は旧来の urn:mace:dir:attribute-def: 表記)で、識別子である eduPersonPrincipalName のほうが圧倒的に多く要求されています。誰であるかを渡してから SP 側で判断する運用が主流のようです。

実例: mdx にログインするとき何が起きているか

ここまでは全体の集計でした。1 つのサービスに絞って、名簿のどの記述が効いているかを追います。題材は mdx(データ活用社会創成プラットフォーム)です。名前に mdx を含むエンティティを引いてみます。

python3 -c '
import xml.etree.ElementTree as ET
MD = "{urn:oasis:names:tc:SAML:2.0:metadata}"
r = ET.parse("gakunin.xml").getroot()
for e in r.findall(MD + "EntityDescriptor"):
    eid = e.get("entityID") or ""
    if "mdx" not in eid:
        continue
    role = "SP" if e.find(MD + "SPSSODescriptor") is not None else "IdP"
    org = e.find(MD + "Organization/" + MD + "OrganizationDisplayName")
    print(role, eid, "|", org.text if org is not None else "")
'
SP https://prjpl.mdx.jp/shibboleth-sp | mdx
SP https://oprpl.mdx.jp/shibboleth-sp | mdx
SP https://docs.mdx.jp/shibboleth-sp | mdx
SP https://project.osaka.mdx.jp/shibboleth-sp | mdxII project application form
SP https://portal.osaka.mdx.jp/shibboleth-sp | mdxII (データ活用社会創成プラットフォーム基盤高度化システム) ユーザポータル
SP https://sp.maas.mdx1.jp/shibboleth-sp | mdx-secure-maas

6 件あり、すべて SP でした。つまり mdx は自前で利用者を認証せず、各大学の IdP に任せる側に回っています。entityIDshibboleth-sp で終わっているのは、Shibboleth の SP をそのまま立てた構成であることを示しています。運営は東京大学と大阪大学に分かれています。

このうち 1 件を開きます。

python3 -c '
import xml.etree.ElementTree as ET
MD = "{urn:oasis:names:tc:SAML:2.0:metadata}"
r = ET.parse("gakunin.xml").getroot()
for e in r.findall(MD + "EntityDescriptor"):
    if e.get("entityID") != "https://sp.maas.mdx1.jp/shibboleth-sp":
        continue
    d = e.find(MD + "SPSSODescriptor")
    for acs in d.findall(MD + "AssertionConsumerService"):
        print(acs.get("Binding").split(":")[-1], acs.get("Location"))
    for ra in e.iter(MD + "RequestedAttribute"):
        print(ra.get("FriendlyName"), "isRequired=" + str(ra.get("isRequired")))
'
HTTP-POST https://sp.maas.mdx1.jp/Shibboleth.sso/SAML2/POST
HTTP-POST-SimpleSign https://sp.maas.mdx1.jp/Shibboleth.sso/SAML2/POST-SimpleSign
HTTP-Artifact https://sp.maas.mdx1.jp/Shibboleth.sso/SAML2/Artifact
browser-post https://sp.maas.mdx1.jp/Shibboleth.sso/SAML/POST
artifact-01 https://sp.maas.mdx1.jp/Shibboleth.sso/SAML/Artifact
eduPersonPrincipalName isRequired=true
displayName isRequired=None
mail isRequired=None

必須と宣言されているのは eduPersonPrincipalName だけで、displayNamemail には isRequired が付いていません。このサービスが所属大学に求めているのは、実質「スコープ内で一意な識別子ひとつ」です。 役割を示す eduPersonAffiliation も、所属を示す eduPersonScopedAffiliation も要求していません。前節で見た「誰であるかを渡してから SP 側で判断する」運用の実例になっています。

受け取り口が 5 つ並んでいるのは、SAML 2.0 の 3 方式に加えて SAML 1.x 世代の 2 方式も残しているためです。後者の識別子が urn:oasis:names:tc:SAML:1.0:profiles: のままなのは歴史的な経緯で、SAML 1.1 の仕様が 1.0 の URI を引き継いでいるためです。また 2.0 側の 3 つが結合方式(Binding)の識別子なのに対し、こちらはプロファイルの識別子で、それを Binding 属性に書くことがメタデータの仕様で定められています。実際に使われるのは通常いちばん上の HTTP-POST です。

利用者側の IdP

利用者が東京大学の所属だとします。対応する IdP は次の 1 件です。

python3 -c '
import xml.etree.ElementTree as ET
MD = "{urn:oasis:names:tc:SAML:2.0:metadata}"
SHIBMD = "{urn:mace:shibboleth:metadata:1.0}"
r = ET.parse("gakunin.xml").getroot()
for e in r.findall(MD + "EntityDescriptor"):
    if e.get("entityID") != "https://gidp.adm.u-tokyo.ac.jp/idp/shibboleth":
        continue
    for s in e.iter(SHIBMD + "Scope"):
        print("Scope", s.text)
    d = e.find(MD + "IDPSSODescriptor")
    for sso in d.findall(MD + "SingleSignOnService"):
        print(sso.get("Binding").split(":")[-1], sso.get("Location"))
'
Scope u-tokyo.ac.jp
Scope u-tokyo.ac.jp
AuthnRequest https://gidp.adm.u-tokyo.ac.jp/idp/profile/Shibboleth/SSO
HTTP-POST https://gidp.adm.u-tokyo.ac.jp/idp/profile/SAML2/POST/SSO
HTTP-Redirect https://gidp.adm.u-tokyo.ac.jp/idp/profile/SAML2/Redirect/SSO

Scope が 2 回出るのは、この IdP が SSO 用IDPSSODescriptor)と属性問い合わせ用AttributeAuthorityDescriptor)の 2 つの役割を宣言していて、それぞれの Extensions に 1 つずつ入っているためです。学認の IdP 419 件のうち、この 2 役を持つ 286 件が Scope 2 個、SSO だけの 133 件が 1 個で、内訳がぴたりと一致します。

訂正(公開後に検証しました)。当初ここには「署名用と暗号用の記述それぞれに宣言が入っているため」と書いていました。誤りですshibmd:Scope は役割の記述子の Extensions に置かれる要素で、KeyDescriptor の中には入りません(学認のメタデータ全体で 0 件)。そもそもこの IdP の KeyDescriptor は 4 つとも use 属性を持たず、署名用と暗号用の区別が書かれていません。

値はどちらも u-tokyo.ac.jp で、この IdP が名乗ってよいのはこのドメインまでという宣言です。

一往復で何が起きるか

これで両側の記述が揃いました。実際の流れはこうなります。

① 利用者が https://sp.maas.mdx1.jp/ を開く
② mdx の SP が所属機関を尋ね、東京大学が選ばれる
     → 名簿から SSO の送り先を引く
       https://gidp.adm.u-tokyo.ac.jp/idp/profile/SAML2/Redirect/SSO
③ 東京大学の IdP が利用者を認証する(ここは学内の話。mdx は関与しない)
④ IdP がアサーションに署名し、名簿にある受け取り口へブラウザ経由で送る
     → https://sp.maas.mdx1.jp/Shibboleth.sso/SAML2/POST
       中身は eduPersonPrincipalName = <利用者名>@u-tokyo.ac.jp など
⑤ mdx の SP が、名簿にある東京大学の鍵で署名を検証する
     さらに @ の右側が Scope(u-tokyo.ac.jp)と一致するかを確かめる
⑥ 通れば「東京大学の、この識別子の人」として mdx に到達する

注目したいのは、mdx と東京大学が互いに登録していないことです。②で送り先を知るのも、⑤で鍵を知るのも、どちらも名簿からです。学認は名簿を配っただけで、①〜⑥の通信のどこにも登場しません。ブラウザが証明書を検証するときにルート CA へ問い合わせないのと同じ構図です。

「認証は通ったが、権限がない」

ここからが実務で効いてくる話です。⑥まで到達しても、サービスが使えるとは限りません。実際、mdx の入口に直接アクセスすると権限がない旨の応答が返ることがあります。

名簿を読み直すと理由が見えます。mdx の SP が要求しているのは eduPersonPrincipalName だけでした。これは「その人が誰か」を運ぶだけで、「その人が mdx を使ってよいか」は何も語りません。

学認が運ぶ範囲   この利用者は <利用者名>@u-tokyo.ac.jp である       ← 認証
mdx が決める範囲  その識別子にプロジェクトが割り当てられているか   ← 認可

計算資源の割り当ては申請と審査を経て決まるもので、大学に所属していれば自動的に付くものではありません。だから所属を示す属性を要求する必要がなく、識別子ひとつで足ります。受け取った識別子を自分の台帳と突き合わせるのは SP 側の仕事だからです。

前節で eduPersonScopedAffiliation を要求する SP が 16 件しかなかったのは、この形が多数派であることの裏返しでもあります。所属を条件にするサービス(電子ジャーナルなど)と、個別の割り当てを条件にするサービス(計算資源など)とで、必要な属性が違います。

なお、mdx 側の認可の規則そのものは公開情報から確認していません。ここで述べたのは、名簿に書かれた要求属性から読み取れる範囲と、認証と認可が別の層であるという一般論です。

名簿を信じてよい根拠

ここまで見た内容は、ネットワーク越しに取得した XML に書いてあることにすぎません。それを信じてよい根拠は、XML 全体に付いている署名です。署名者の証明書は XML の中に埋め込まれています。

python3 -c '
import re, base64
d = open("gakunin.xml", encoding="utf-8").read()
m = re.search(r"<ds:X509Certificate>([^<]+)</ds:X509Certificate>", d)
open("signer.der", "wb").write(base64.b64decode("".join(m.group(1).split())))
'
openssl x509 -inform DER -in signer.der -noout -subject -issuer -dates
subject= /C=JP/O=GakuNin - Japanese Academic Access Management Federation/CN=GakuNin Metadata Signer - G2
issuer= /C=JP/O=GakuNin - Japanese Academic Access Management Federation/CN=GakuNin Metadata Signer - G2
notBefore=Nov 14 07:04:41 2017 GMT
notAfter=Nov 14 07:04:41 2027 GMT

上の出力は macOS 標準の LibreSSL のものです。Homebrew の OpenSSL 3 では subject=C=JP, O=…, CN=… の形式になりますが、値は同じです。

subject と issuer が同じ、つまり自己署名です。公的な認証局から辿れるわけではなく、参加機関が「これを信じる」と決めて事前に手元へ置くことで成立しています。この位置にある鍵をトラストアンカーと呼びます。

実際の運用では、埋め込まれた証明書をそのまま信じるのではなく、別途配布された署名者証明書のフィンガープリントと突き合わせる設定にします。埋め込みの証明書だけを見て検証すると、名簿ごと差し替えられたときに気づけないためです。

Verifiable Credentials との違い

この構造を踏まえると、三者モデルとの違いがはっきりします。

SAML(学認)Verifiable Credentials
属性が届く経路IdP から SP へ直接発行者 → 本人 → 検証者
事前共有するもの参加者全員のメタデータどの発行者を信じるかの一覧
提示できる相手名簿に載っている相手名簿の外にも提示できる
属性を選ぶ主体IdP の送出ポリシー本人(ウォレット)
ログインのたびに発行元が動くか動く動かない(事前発行)

「名簿に載っている相手にしか出せない」という制約は、裏返せば「名簿に載っていれば相手の素性が分かる」という利点でもあります。学認が 2009 年の試行運用開始から 15 年以上かけて積み上げたのはこの名簿の運用そのもので、Verifiable Credentials の側で課題として残っている部分にあたります。

触れていないもの

対面提示を扱う ISO/IEC 18013-5(mdoc、スマートフォンで運転免許証などを提示するための国際規格)については、規格本文が有償なこともあり、この記事では扱っていません。学認のトラストフレームワークを参考にした共同研究(NII と伊藤忠テクノソリューションズ、2024 年 10 月〜2026 年 3 月)でも、W3C の Verifiable Credentials と mdoc のどちらを採るかは未決とされていたので、そこは別途調べる必要があります。

出典