PowerShellでMicrosoft Defender for Endpoint APIにJWT認証する方法

# はじめに

## 目的
この記事はローカルのPowerShellスクリプトから、Microsoft Defender for EndpointのAPIにJWT認証でアクセスすることが目的です。

Defender for Endpointと言いつつ実質的にDefenderポータルの機能にアクセス可能です。例えばデバイス一覧を取得するURLはこちらです。
```
https://api.securitycenter.microsoft.com/api/machines
```
[参考]
[Supported Microsoft Defender for Endpoint APIs](https://learn.microsoft.com/en-us/defender-endpoint/api/exposed-apis-list)

## 背景
まずEntra ID上にMicrosoft Defender for Endpointへアクセスするためのアプリケーション(サービスプリンシパル)を登録しますが、その際、資格情報として証明書とクライアントシークレットを使えます。

クライアントシークレットの認証手順の方がシンプルなためこちらを選ぶことが多いと思いますが、今回はアクセス元ホストを制限するためにあえて証明書を使用します。

そして証明書認証を使用するとJWT認証を使う必要が出てくるため、PowerShellでJWT認証するためのスクリプトを書くことになるというわけです。

ちなみにクライアントシークレットを使用する場合はこのように非常にシンプルです。

```PowerShell:証明書ではなくクライアントシークレットを使用する場合
$TenantId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
$AppId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
$AppSecret = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
$Scope = 'https://api.securitycenter.microsoft.com'    
$AuthUri = "https://login.microsoftonline.com/$tenantId/oauth2/token"
$authBody = [Ordered] @{
    resource = "$Scope"
    client_id = "$AppId"
    client_secret = "$AppSecret"
    grant_type = 'client_credentials'
}
$res = Invoke-RestMethod -Method Post -Uri $AuthUri -Body $authBody -ErrorAction Stop
```
クライアントシークレットの場合はこのようにシンプルですが、証明書認証の場合JWT(JSON Web Token)を作成する必要があるため、非常に複雑なスクリプトになります。

JWTの詳細についてここでは説明しませんが、JWT生成の大まかな流れは以下のとおりです。
- JWTヘッダとJWTペイロードを作成
- JWTヘッダ、JWTペイロードをそれぞれをBase64エンコードしてドット「.」で接続
- その全体を証明書の秘密鍵で署名してBase64エンコードし、JWT署名を作成
- 最後にJWTヘッダ、JWTペイロード、JWT署名をドット「.」で接続

## 参考にした記事
この記事は以下の記事を参考にしています。

[How to get access token from client certificate? Can we use client thumbprint directly?](https://learn.microsoft.com/en-us/answers/questions/346048/how-to-get-access-token-from-client-certificate-ca)

# 事前準備

## サービスプリンシパルの作成
詳細は省略します。Entra IDから一般的な手順で「アプリの登録」をしてください。

## 自己署名証明書の作成
手動で作成する方法もありますが、以下のPowerShellスクリプトで簡単に作成します。後でサービスプリンシパルにアップロードする必要があるため「.cer」形式ファイルにエクスポートもしておきます。
```PowerShell:自己署名証明書の作成
$certname = "SampleCertificate"
$cert = New-SelfSignedCertificate -Subject "CN=$certname" `
    -CertStoreLocation "Cert:\CurrentUser\My" `
    -KeyExportPolicy Exportable -KeySpec Signature -KeyLength 2048 `
    -KeyAlgorithm RSA -HashAlgorithm SHA256
Export-Certificate -Cert $cert -FilePath ("D:\Temp\" + $certname + ".cer")
```
作成された証明書の拇印を後ほど使いますので、「ユーザー証明書の管理」から確認しておきます。(スタートメニューから「ファイル名を指定して実行」を選択し「certmgr.msc」で起動)

「証明書 - 現在のユーザー＞個人＞証明書」を開き、上で作成した証明書をダブルクリックで開いて「詳細」タブのフィールド「拇印」で確認します。

## 証明書のアップロード
Entra IDで先ほど作成したアプリを開き、左メニュー「証明書とシークレット」をクリック、「証明書」タブの「証明書のアップロード」から、上記で書き出した「.cer」形式ファイルを選択、アップロードします。

以上で準備ができました。

# PowerShellスクリプトの作成

## 定数類の定義
まずスクリプトの冒頭で認証に使う各種定数を定義します。

```PowerShell:各種定数類の定義
# テナントID
$TenantId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
# アプリ(サービスプリンシパル)ID
$AppId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
# Microsoft Defenderのapi URL
$Scope = "https://api.securitycenter.microsoft.com"
# OAuth認証のためのURL
$AuthUri = "https://login.microsoftonline.com/$TenantId/oauth2/token"
```

## JWTヘッダの作成

### 証明書のハッシュを取得
作成した証明書の拇印で証明書を取得し、そのハッシュ値をBase64エンコードした文字列を作成します。

```PowerShell:証明書のハッシュ値をBase64エンコード
$Certificate = Get-Item -Path "Cert:\CurrentUser\My\(40桁英数字の拇印)"
$CertificateBase64Hash = [System.Convert]::ToBase64String($Certificate.GetCertHash())
```

証明書を取得する際に以下の方法を使うとエラーになるため要注意です。

```PowerShell:証明書の誤った取得方法
# 誤った例
$Certificate = Get-ChildItem -Path "Cert:\CurrentUser\My" |
   Where-Object Subject -eq "CN=SampleCertificate"
```
### JWTヘッダの組み立て

JWTヘッダの各行(各クレーム)についての詳細は説明しません。「alg」は後ほど実行する署名のアルゴリズムですが、ここではRSAとSHA256の組み合わせを使用するため「RS256」です。「typ」は「JWT」固定です。

「x5t」に上記で作成した証明書のハッシュ値のBase64エンコード文字列を指定します。

ただし、以下Base64でエンコードされた文字列をJWTに使用する場合は、つねに「+」「/」「=」の3文字を削除する必要があります。そのため`-replace '\+','-' -replace '/','_' -replace '='`という部分が必要になります。

```PowerShell:JWTヘッダの組み立て
$JWTHeader = @{  
    alg = "RS256"  
    typ = "JWT"  
    x5t = $CertificateBase64Hash -replace '\+','-' -replace '/','_' -replace '='  
}
```

## JWTペイロードの作成

### JWT有効期限の指定

JWTペイロードを作成する際はJWTの有効期間を設定する必要があるため、まずその部分から記述します。ここではエポック時間で現時点から2分後までとします。

```PowerShell:JWT有効期間の定義
# 失効日時
$StartDate = (Get-Date "1970-01-01T00:00:00Z" ).ToUniversalTime()  
$JWTExpirationTimeSpan = (New-TimeSpan -Start $StartDate -End (Get-Date).ToUniversalTime().AddMinutes(2)).TotalSeconds  
$JWTExpiration = [math]::Round($JWTExpirationTimeSpan,0)  

# 有効開始日時
$NotBeforeExpirationTimeSpan = (New-TimeSpan -Start $StartDate -End ((Get-Date).ToUniversalTime())).TotalSeconds  
$NotBefore = [math]::Round($NotBeforeExpirationTimeSpan,0)  
```

### JWTペイロードの作成

有効期限が決まればJWTペイロードを作成できます。

```PowerShell:JWTペイロードの作成
$JWTPayLoad = @{  
    # 認証のためのエンドポイント
    aud = $AuthUri  
  
    # Issuer: JWTの発行者 = サービスプリンシパルのアプリケーションID (クライアントID) 
    iss = $AppId  
  
    # JWT Subject: JWTの用途 = サービスプリンシパルのアプリケーションID (クライアントID)
    sub = $AppId  
  
    # Not Before: JWTの有効開始日時 (いつから)
    nbf = $NotBefore  
  
    # Expiration Time: JWTの失効日時 (いつまで) 
    exp = $JWTExpiration  
  
    # JWT ID: ランダムなGUIDを生成 
    jti = [guid]::NewGuid()  
}
```

## JWTヘッダ、JWTペイロードのBase64エンコード

以上で作成したJWTヘッダ、JWTペイロードをBase64エンコードしてドットでつなげます。

```PowerShell:JWTヘッダ、JWTペイロードをBase64エンコードしドットでつなげる
# JWTヘッダをBase64エンコード
$JWTHeaderToByte = [System.Text.Encoding]::UTF8.GetBytes(($JWTHeader | ConvertTo-Json))  
$EncodedHeader = [System.Convert]::ToBase64String($JWTHeaderToByte)  

# JWTペイロードをBase64エンコード
$JWTPayLoadToByte =  [System.Text.Encoding]::UTF8.GetBytes(($JWTPayload | ConvertTo-Json))  
$EncodedPayload = [System.Convert]::ToBase64String($JWTPayLoadToByte)  
  
# Base64エンコードしたJWTヘッダとJWTペイロードをドットでつなげる 
$JWT = $EncodedHeader + "." + $EncodedPayload  
```

ここまででJWTの前半、署名以外の部分が出来ました。次はこの`$JWT`を証明書の秘密鍵で署名したものをBase64エンコードし、$JWTのさらに後ろにドットでつなげます。

## "JWTヘッダ.JWTペイロード"を署名

### 証明書から秘密鍵の取り出し

署名に必要な秘密鍵を証明書から取り出します。証明書は先ほど`$Certificate`という変数に代入してあります。

```PowerShell:証明書から秘密鍵を取り出す
$PrivateKey = ([System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($Certificate)) 
```

### 署名に使うハッシュアルゴリズムとパディング方式を指定

次に、署名に使うハッシュアルゴリズムとパディング方式を指定します。パディングについては正直よく理解していないため各種資料をあたっていただければと思います。

```PowerShell:ハッシュアルゴリズムとパディング方式の指定
$HashAlgorithm = [Security.Cryptography.HashAlgorithmName]::SHA256  
$RSAPadding = [Security.Cryptography.RSASignaturePadding]::Pkcs1  
```

### 署名の作成

これで署名の準備ができたので署名を作成します。

JWTヘッダとJWTペイロードをドット(.)でつなげた文字列を、秘密鍵を使って指定のハッシュアルゴリズムとパティング方式で署名し、Base64エンコードしてから、例によって「+」「/」「=」の3文字を削除します。

```PowerShell:署名の作成
$SignedData = $PrivateKey.SignData([System.Text.Encoding]::UTF8.GetBytes($JWT),$HashAlgorithm,$RSAPadding)
$Signature = [Convert]::ToBase64String($SignedData) -replace '\+','-' -replace '/','_' -replace '='  
```

## JWTの完成

こうして出来た署名をJWTにさらにドットでつなげてJWTの完成です。

```PowerShell:JWTの完成
$JWT = $JWT + "." + $Signature  
```

## トークンの取得

ここまででJWTが完成したので認証エンドポイントに対して認証を実行し、トークンを取得します。認証の実行にはヘッダ部分とボディー部分が必要ですが、JWTはヘッダ部分とボディー部分の両方に使います。

### 認証ヘッダの作成

認証ヘッダは`Authorization`の`Bearer`としてここまでで作成したJWTを渡すだけです。

```PowerShell: 認証ヘッダの作成
$Header = @{  
    Authorization = "Bearer $JWT"  
}  
```

### 認証ボディーの作成

```PowerShell:認証ボディーの作成
$Body = @{
    # サービスプリンシパルのアプリケーションID (クライアントID)
    client_id = $AppId  
    client_assertion = $JWT  
    client_assertion_type = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"  
    resource = $Scope  
    grant_type = "client_credentials"  
}  
```

### 認証の実行とトークンの取得

上記の認証ヘッダ、認証ボディーで認証を実行します。

```PowerShell:認証の実行
$req = Invoke-RestMethod -Uri $AuthUri -Method Post `
    -Headers $Header -Body $Body `
    -ContentType 'application/x-www-form-urlencoded'
$token = $req.access_token
```

以上でDefender for EndpointのAPIを呼び出すためのトークンが取得できました。

# Defender for Endpoint APIの呼び出し

一例としてDefender for Endpointの全アラートを取得してみます。上記で取得したトークンの頭に'Bearer 'を付けて、ヘッダーのAuthorizationに設定して、呼び出すだけです。

アラートを取得するためのAPIのURLは`https://api.securitycenter.microsoft.com/api/alerts`です。

```PowerShell:全アラートの取得
$headers = @{ 
    'Content-Type' = 'application/json'
    Authorization = ("Bearer " + $token)
}
$reqURL = 'https://api.securitycenter.microsoft.com/api/alerts'
$res = Invoke-RestMethod -Uri $reqURL -Headers $headers -Method Get
$items = $res.value
```

別の例として、Defender for Endpointが認識しているデバイス(オンボードされているかどうかにかかわらず)をすべて取得するスクリプトは以下のようになります。APIのURLの末尾を`machines`に変えるだけです。

```PowerShell:全デバイスの取得
$headers = @{ 
    'Content-Type' = 'application/json'
    Authorization = ("Bearer " + $token)
}
$reqURL = 'https://api.securitycenter.microsoft.com/api/machines'
$res = Invoke-RestMethod -Uri $reqURL -Headers $headers -Method Get
$items = $res.value
```

# まとめ

サービスプリンシパルのアプリケーションID(クライアントID)とクライアントシークレットを利用すると、認証の手順は非常にシンプルなのですが、証明書を使う場合はここまで述べたように非常に煩雑になります。

クライアントシークレットは漏えいすると第三者が任意の端末から認証できてしまうため、特定の端末で作成した自己署名証明書による認証の方が、認証元の端末を制限でき、セキュリティレベルは一段階上がります。

サービスプリンシパル経由でAPIを呼び出す端末を限定できる場合は、スクリプトが煩雑になっても、JWTを利用した証明書認証の方がよさそうです。

以上です。