AB#2413: Add workflow for snp-report-verify

* Extend azure-snp-report-verify to also report fw SVNs.
* Add workflow based on azure-cvm to get maa-jwt and
verify it on a second runner.
This commit is contained in:
Otto Bittner 2022-09-21 10:47:38 +02:00
parent d85b281570
commit 0eb4a7831b
6 changed files with 133 additions and 18 deletions

View File

@ -0,0 +1,12 @@
name: Azure SNP Reporter
description: "Get SNP MAA statement from Azure."
inputs:
outputPath:
description: "Path to put signed JWT into."
required: true
runs:
using: "composite" # some azure SNP-enabled machine.
steps:
- name: Fetch report
shell: bash
run: docker run --rm --privileged -v/sys/kernel/security:/sys/kernel/security ghcr.io/edgelesssys/constellation/azure-snp-reporter | tail -n 1 > ${{ inputs.outputPath }}

View File

@ -5,6 +5,10 @@ This folder contains the files to setup an Azure function and ARM template in or
- `azure-function`: All necessary files to redeploy the function. Changes in `requirements.txt` are installed during deployment of the function. `cloud-init.txt` is put into the CVM by supplying it as a parameter to the ARM template deployment. - `azure-function`: All necessary files to redeploy the function. Changes in `requirements.txt` are installed during deployment of the function. `cloud-init.txt` is put into the CVM by supplying it as a parameter to the ARM template deployment.
# Update cvm-template # Update cvm-template
In order to make the Azure function use your changes you will have to publish them in the `cvm-template.json` file.
While developing you can point the `template_id` variable in `__init__.py` to a different location.
Doing the following you can debug your template changes:
- Look for the `Template spec` resource in your Azure project (e.g. "snp-value-reporter-template"). - Look for the `Template spec` resource in your Azure project (e.g. "snp-value-reporter-template").
- Click on "Create new version". - Click on "Create new version".
- Select the latest version available. - Select the latest version available.

View File

@ -0,0 +1,81 @@
name: Fetch, validate and report SNP report data.
on:
workflow_dispatch:
schedule:
- cron: "0 14 * * 0"
# Abort runs of *this* workflow, if a new commit with the same ref is pushed that is not main.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
build-snp-reporter:
name: "Build SNP-reporter container"
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b
- name: Set up Go
uses: actions/setup-go@268d8c0ca0432bb2cf416faae41297df9d262d7f
with:
go-version: 1.18
- name: Build and upload azure SNP reporter container image
id: build-and-upload
uses: ./.github/actions/build_micro_service
with:
name: azure-snp-reporter
dockerfile: ./hack/azure-snp-report-verify/Dockerfile
githubToken: ${{ secrets.GITHUB_TOKEN }}
fetch-snp-report:
needs: build-snp-reporter
name: "Fetch SNP report"
runs-on: [self-hosted, azure-cvm]
env:
SHELL: /bin/bash
steps:
- name: Checkout
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b
- name: Fetch SNP report
uses: ./.github/actions/azure_snp_reporter
with:
outputPath: ${{ github.workspace }}/maa-report.jwt
- name: Upload report JWT
uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8
with:
name: maa-report.jwt
path: "${{ github.workspace }}/maa-report.jwt"
validate-snp-report:
needs: fetch-snp-report
name: "Validate SNP report"
runs-on: ubuntu-latest
env:
SHELL: /bin/bash
steps:
- name: Checkout
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b
with:
submodules: recursive
token: ${{ secrets.CI_GITHUB_REPOSITORY }}
- name: Set up Go
uses: actions/setup-go@268d8c0ca0432bb2cf416faae41297df9d262d7f
with:
go-version: 1.18
- name: Download report JWT
uses: actions/download-artifact@v3
with:
name: "maa-report.jwt"
path: "."
- name: Verify report
shell: bash
run: go run ./hack/azure-snp-report-verify/verify.go $(cat ./maa-report.jwt)

View File

@ -10,7 +10,7 @@ RUN wget -q https://github.com/Azure/confidential-computing-cvm-guest-attestatio
&& touch Utils.h \ && touch Utils.h \
&& g++ -Os -I/usr/include/azguestattestation1 -oclient main.cpp -lazguestattestation && g++ -Os -I/usr/include/azguestattestation1 -oclient main.cpp -lazguestattestation
FROM ubuntu:20.04 FROM ubuntu:20.04 AS release
COPY --from=build client azguestattestation1_1.0.2_amd64.deb / COPY --from=build client azguestattestation1_1.0.2_amd64.deb /
RUN apt-get update && apt-get install -y /azguestattestation1_1.0.2_amd64.deb RUN apt-get update && apt-get install -y /azguestattestation1_1.0.2_amd64.deb
ENTRYPOINT ["/client"] ENTRYPOINT ["/client"]

View File

@ -1,20 +1,21 @@
# Obtain current Azure SNP ID key digest # Obtain current Azure SNP ID key digest & firmware versions
On Azure, Constellation verifies that the SNP attestation report contains Azure's ID key digest. On Azure, Constellation verifies that the SNP attestation report contains Azure's ID key digest.
Currently, the only way to verify this digest's origin is to perform guest attestation with the help of the Microsoft Azure Attestation (MAA) service. Additionally, some firmware security version numbers (SVNs) are validated.
Currently, the only way to verify the digest's origin is to perform guest attestation with the help of the Microsoft Azure Attestation (MAA) service.
There's a [sample](https://github.com/Azure/confidential-computing-cvm-guest-attestation) on how to do this, but it's not straightforward. There's a [sample](https://github.com/Azure/confidential-computing-cvm-guest-attestation) on how to do this, but it's not straightforward.
So we created tooling to make things easier. So we created tooling to make things easier.
Perform the following steps to get the ID key digest: Perform the following steps to get the ID key digest & firmware versions:
1. Create an Ubuntu CVM on Azure with secure boot enabled and ssh into it. 1. Create an Ubuntu CVM on Azure with secure boot enabled and ssh into it.
2. Run 2. Run
``` ```
docker run --rm --privileged -v/sys/kernel/security:/sys/kernel/security ghcr.io/edgelesssys/constellation/get-azure-snp-jwt docker run --rm --privileged -v/sys/kernel/security:/sys/kernel/security ghcr.io/edgelesssys/constellation/azure-snp-reporter
``` ```
This executes the guest attestation and prints the JWT received from the MAA. (It's the long base64 blob.) This executes the guest attestation and prints the JWT received from the MAA. (It's the long base64 blob.)
3. Copy the JWT and run **on your local trusted machine**: 3. Copy the JWT and run **on your local trusted machine**:
``` ```
go run verify.go <jwt> go run verify.go <jwt>
``` ```
On success it prints the ID key digest. On success it prints the ID key digest and relevant firmware SVNs.

View File

@ -23,33 +23,52 @@ import (
"gopkg.in/square/go-jose.v2/jwt" "gopkg.in/square/go-jose.v2/jwt"
) )
type IsolationTEE struct {
IDKeyDigest string `json:"x-ms-sevsnpvm-idkeydigest"`
TEESvn int `json:"x-ms-sevsnpvm-tee-svn"`
SNPFwSvn int `json:"x-ms-sevsnpvm-snpfw-svn"`
MicrocodeSvn int `json:"x-ms-sevsnpvm-microcode-svn"`
BootloaderSvn int `json:"x-ms-sevsnpvm-bootloader-svn"`
GuestSvn int `json:"x-ms-sevsnpvm-guestsvn"`
}
func (i *IsolationTEE) PrintSVNs() {
fmt.Println("\tTEE SVN:", i.TEESvn)
fmt.Println("\tSNP FW SVN:", i.SNPFwSvn)
fmt.Println("\tMicrocode SVN:", i.MicrocodeSvn)
fmt.Println("\tBootloader SVN:", i.BootloaderSvn)
fmt.Println("\tGuest SVN:", i.GuestSvn)
}
func main() { func main() {
if len(os.Args) != 2 { if len(os.Args) != 2 {
fmt.Println("Usage:", os.Args[0], "<maa-jwt>") fmt.Println("Usage:", os.Args[0], "<maa-jwt>")
return return
} }
idKeyDigest, err := getIDKeyDigest(os.Args[1]) report, err := getTEEReport(os.Args[1])
if err != nil { if err != nil {
panic(err) panic(err)
} }
fmt.Println("Successfully validated ID key digest:", idKeyDigest) fmt.Println("Successfully validated ID key digest:", report.IDKeyDigest)
fmt.Println("Currently reported SVNs:")
report.PrintSVNs()
} }
func getIDKeyDigest(rawToken string) (string, error) { func getTEEReport(rawToken string) (IsolationTEE, error) {
// Parse token. // Parse token.
token, err := jwt.ParseSigned(rawToken) token, err := jwt.ParseSigned(rawToken)
if err != nil { if err != nil {
return "", err return IsolationTEE{}, err
} }
// Get JSON Web Key set. // Get JSON Web Key set.
keySetBytes, err := httpGet(context.Background(), "https://sharedeus.eus.attest.azure.net/certs") keySetBytes, err := httpGet(context.Background(), "https://sharedeus.eus.attest.azure.net/certs")
if err != nil { if err != nil {
return "", err return IsolationTEE{}, err
} }
keySet, err := parseKeySet(keySetBytes) keySet, err := parseKeySet(keySetBytes)
if err != nil { if err != nil {
return "", err return IsolationTEE{}, err
} }
// Get claims. Private claims contain ID Key digest. // Get claims. Private claims contain ID Key digest.
@ -57,19 +76,17 @@ func getIDKeyDigest(rawToken string) (string, error) {
var publicClaims jwt.Claims var publicClaims jwt.Claims
var privateClaims struct { var privateClaims struct {
IsolationTEE struct { IsolationTEE IsolationTEE `json:"x-ms-isolation-tee"`
IDKeyDigest string `json:"x-ms-sevsnpvm-idkeydigest"`
} `json:"x-ms-isolation-tee"`
} }
if err := token.Claims(&keySet, &publicClaims, &privateClaims); err != nil { if err := token.Claims(&keySet, &publicClaims, &privateClaims); err != nil {
return "", err return IsolationTEE{}, err
} }
if err := publicClaims.Validate(jwt.Expected{Time: time.Now()}); err != nil { if err := publicClaims.Validate(jwt.Expected{Time: time.Now()}); err != nil {
return "", err return IsolationTEE{}, err
} }
return privateClaims.IsolationTEE.IDKeyDigest, nil return privateClaims.IsolationTEE, nil
} }
func parseKeySet(keySetBytes []byte) (jose.JSONWebKeySet, error) { func parseKeySet(keySetBytes []byte) (jose.JSONWebKeySet, error) {