diff --git a/labs/GSP1302/HASHNODE.md b/labs/GSP1302/HASHNODE.md new file mode 100644 index 0000000..ed916e0 --- /dev/null +++ b/labs/GSP1302/HASHNODE.md @@ -0,0 +1,214 @@ +# Leveraging Verifiable Credentials and Zero-Knowledge Proofs with Sindri - GSP1302 + +## Overview + +This lab demonstrates digital university degree credentials and zero-knowledge proofs. The issuer signs a degree claim; the holder produces a presentation; the verifier checks the claim without receiving the original signature. The implementation uses the public Sindri codelab, its Go-based BLS signer/verifier, and the Sindri proof service. + +There are **five official Tasks and four graded checkpoints** worth 30/20/25/25 points. The Sindri API key and Cloud Storage bucket are assigned by the active lab. Never publish those values. + +The official [credential lifecycle and presentation diagrams](https://www.skills.google/focuses/118451?parent=catalog) are linked rather than copied because image embedding rights have not been confirmed. + +### What you'll do + +- Install Sindri CLI and save its version. +- Clone the upstream issuer/verifier example and upload the first checkpoint artifacts. +- Sign a degree credential and save its BLS witness. +- Clone the public zero-knowledge circuit and generate a proof. +- Verify the proof with Go and upload the verification result. + +## Setup and requirements + +### Before you click the Start Lab button + +Open [the official GSP1302 lab](https://www.skills.google/focuses/118451?parent=catalog) and start it when you have time for all tasks. Use a private browser and the **temporary student account** supplied by the active lab, not a personal Google Cloud account. Do not copy the session's username, password, project ID, API key, or other credentials into GitHub. + +Record the lab-provided **Sindri API key** and **Cloud Storage bucket name** from the active lab panel. The API key authenticates with the Sindri service; the bucket stores grader artifacts. + +### Activate Cloud Shell + +Activate Cloud Shell in the Google Cloud console, authorize the temporary account, and verify the project and tools: + +```bash +gcloud auth list +gcloud config get-value project +for tool in gcloud gsutil git go npm jq; do command -v "$tool" || exit 1; done +``` + +Provide the Sindri key privately without printing it: + +```bash +read -r -s -p "Sindri API key from the active lab: " SINDRI_API_KEY +printf '\n' +export SINDRI_API_KEY +``` + +Identify the lab-provided bucket (do not create a replacement bucket). Use the name shown in the lab panel: + +```bash +export PROJECT_ID="$(gcloud config get-value project)" +gcloud storage buckets list --project="$PROJECT_ID" +read -r -p "Lab bucket name (without gs://): " GCS_BUCKET +export GCS_BUCKET +gsutil ls -b "gs://$GCS_BUCKET" +``` + +## Task 1. Install the Sindri CLI + +Install the published Sindri CLI, then record the version in the home directory. The original lab uses `@latest`; the automation pins the publicly published `0.0.1-alpha.73` release to reduce changes between runs. + +```bash +npm install -g sindri@0.0.1-alpha.73 +sindri --version | tee "$HOME/sindri.txt" +test -s "$HOME/sindri.txt" +``` + +The version text varies by release. The grader expects a `sindri.txt` object in the lab bucket. + +## Task 2. Clone the Source Repository + +Clone the upstream example, then inspect the issuer and verifier files: + +```bash +cd "$HOME" +git clone https://github.com/Sindri-Labs/verifiable-credentials-codelab.git +cd verifiable-credentials-codelab +test -s issuer/issuer.go +test -s issuer/credential.json +test -s verifier/bls-verify.go +``` + +On reruns, verify an existing checkout's origin instead of deleting or overwriting it: + +```bash +git -C "$HOME/verifiable-credentials-codelab" remote get-url origin +``` + +Upload the two artifacts required by the first checkpoint and check their presence: + +```bash +gsutil cp "$HOME/sindri.txt" "gs://$GCS_BUCKET/sindri.txt" +gsutil cp "$HOME/verifiable-credentials-codelab/issuer/issuer.go" "gs://$GCS_BUCKET/issuer.go" +gsutil stat "gs://$GCS_BUCKET/sindri.txt" +gsutil stat "gs://$GCS_BUCKET/issuer.go" +``` + +### Test completed task + +Click **Check my progress** for **Install the Sindri CLI and Clone the Source Repository** (30 points). The uploaded objects can be inspected with `gsutil stat`, but their presence does not establish a grader PASS. + +## Task 3. Issue a Verifiable Credential + +The example issuer reads `credential.json`, creates or reuses a local private signing key, and signs the `BachelorDegree` claim. It writes a signed credential and a separate witness containing the BLS group elements. + +```bash +cd "$HOME/verifiable-credentials-codelab/issuer" +go run issuer.go issuer_sk.txt credential.json +``` + +The private signing key `issuer_sk.txt` stays local. Do **not** upload it to Cloud Storage or GitHub. + +| Output artifact | Meaning | +|---|---| +| `credential-signed.json` | Degree credential with issuer signature metadata | +| `credential-witness-1-degreeType.json` | Witness containing `Sig`, `G2`, `Hm` and `Pk` | + +Verify and upload the expected files: + +```bash +jq -e '.credentialSubject.degreeType == "BachelorDegree"' credential-signed.json +jq -e '.Sig != null and .G2 != null and .Hm != null and .Pk != null' credential-witness-1-degreeType.json +gsutil cp credential-signed.json "gs://$GCS_BUCKET/credential-signed.json" +gsutil cp credential-witness-1-degreeType.json "gs://$GCS_BUCKET/credential-witness-1-degreeType.json" +``` + +The issuer signs the claim, not the entire identity of the holder. The next task uses the witness to prove the signature relationship without exposing the signature itself. + +### Test completed task + +Click **Check my progress** for **Issue a Verifiable Credential** (20 points). Check that both files exist in the lab bucket. + +## Task 4. Build a Verifiable Presentation + +The holder's presentation contains a zero-knowledge proof. Clone the exact public Sindri circuit identified in the official instructions: + +```bash +cd "$HOME/verifiable-credentials-codelab" +sindri clone 42543290-0746-48d5-998a-e39197e5cf79 holder +test -s holder/sindri.json +``` + +If `holder` already exists, inspect `holder/sindri.json` and reuse the directory instead of overwriting it. The `codelab` tag selects the published circuit build. + +Generate the proof from Task 3's witness: + +```bash +cd "$HOME/verifiable-credentials-codelab/holder" +sindri proof create --tag codelab \ + --input ../issuer/credential-witness-1-degreeType.json \ + > credential-proof.json +jq -e '(.proof.proof | type) == "string" and (.verification_key.verifying_key | type) == "string"' credential-proof.json +gsutil cp credential-proof.json "gs://$GCS_BUCKET/credential-proof.json" +``` + +The proof response contains a proof, public inputs, and a verification key. The original private signature is not supposed to be disclosed as the presentation. See the [official illustration of a credential versus a presentation](https://www.skills.google/focuses/118451?parent=catalog); image reuse rights have not been verified. + +### Test completed task + +Click **Check my progress** for **Build a Verifiable Presentation** (25 points). An accepted Sindri API request does not automatically imply a grader PASS. + +## Task 5. Verify the Presentation + +The upstream verifier checks the proof against a pinned verification key and public inputs. Run the actual Go source file `bls-verify.go`: + +```bash +cd "$HOME/verifiable-credentials-codelab/verifier" +go run bls-verify.go ../holder/credential-proof.json | tee verify_response.txt +grep -F 'Proof verified successfully.' verify_response.txt +gsutil cp verify_response.txt "gs://$GCS_BUCKET/verify_response.txt" +gsutil stat "gs://$GCS_BUCKET/verify_response.txt" +``` + +The public verifier prints the following when verification succeeds: + +```plaintext +Proof verified successfully. +``` + +If the verifier fails, inspect the proof response, the circuit identifier, and the pinned key; do not upload a fabricated success message. + +### Test completed task + +Click **Check my progress** for **Verify the Presentation** (25 points). All four checkpoints total 100 points (30 + 20 + 25 + 25). This guide does not claim any actual grader result. + +--- + +## Solution of Lab + +### Quick + +These two commands are intended for the **temporary Google Skills Cloud Shell**. The URL deliberately points to `main` and **will only work after the Draft PR is merged**. The script privately prompts for the Sindri key and, if necessary, the lab bucket name. + +```bash +curl -LO https://raw.githubusercontent.com/ePlus-DEV/storage/refs/heads/main/labs/GSP1302/lab.sh +source lab.sh +``` + +The script checks prerequisites, detects the project and existing bucket, executes Tasks 1–5, and checks the required output objects. It does not run Google Skills' Check my progress buttons. + +### Manual + +Chưa tìm thấy video YouTube hướng dẫn thủ công đã xác minh cho lab này. + +Use the complete manual commands in Tasks 1–5 above and the [official GSP1302 instructions](https://www.skills.google/focuses/118451?parent=catalog). + +## Sources and verification status + +- [Google Skills — GSP1302 official lab](https://www.skills.google/focuses/118451?parent=catalog), last tested February 19, 2025. +- [Sindri-Labs/verifiable-credentials-codelab](https://github.com/Sindri-Labs/verifiable-credentials-codelab) — public upstream issuer and verifier, used at runtime, not copied into this repository. +- [Sindri-Labs/sindri-js](https://github.com/Sindri-Labs/sindri-js) — public CLI implementation and command flags. +- [Sindri npm package](https://www.npmjs.com/package/sindri) — published CLI version. +- [Cloud Storage gsutil documentation](https://cloud.google.com/storage/docs/gsutil/commands/cp). + +**static validated only; runtime/grader unverified.** The latest PR head must pass GitHub CI Bash syntax and ShellCheck severity error. The Sindri service, circuit accessibility, lab-issued key, and grader behavior have not been tested with authorized credentials. No private signing keys or temporary credentials are included. This Markdown has not been published to Hashnode. + +Copyright (c) 2026 ePlus.DEV | Unlicense — original automation and explanatory prose. diff --git a/labs/GSP1302/lab.sh b/labs/GSP1302/lab.sh new file mode 100644 index 0000000..1026ec1 --- /dev/null +++ b/labs/GSP1302/lab.sh @@ -0,0 +1,156 @@ +#!/usr/bin/env bash +# GSP1302 — Leveraging Verifiable Credentials and Zero-Knowledge Proofs with Sindri +# Copyright (c) 2026 ePlus.DEV | Unlicense +# static validated only; runtime/grader unverified +set -Eeuo pipefail +umask 077 + +if [[ -n "${NO_COLOR+x}" ]]; then + CYAN='' GREEN='' YELLOW='' RESET='' +elif [[ -t 1 || "${FORCE_COLOR:-0}" == 1 ]]; then + CYAN=$'\033[36m'; GREEN=$'\033[32m'; YELLOW=$'\033[33m'; RESET=$'\033[0m' +else + CYAN='' GREEN='' YELLOW='' RESET='' +fi +info() { printf '%s[INFO]%s %s\n' "$CYAN" "$RESET" "$*"; } +ok() { printf '%s[OK]%s %s\n' "$GREEN" "$RESET" "$*"; } +warn() { printf '%s[WARN]%s %s\n' "$YELLOW" "$RESET" "$*" >&2; } +die() { printf '[ERROR] %s\n' "$*" >&2; exit 1; } +trap 'warn "Execution failed near line $LINENO; inspect the preceding error (no grader PASS is implied)."' ERR +printf '%sePlus.DEV – Starting Execution%s\n' "$CYAN" "$RESET" +printf 'Copyright (c) 2026 ePlus.DEV | Unlicense\n' + +for tool in gcloud gsutil git go npm jq; do + command -v "$tool" >/dev/null 2>&1 || die "Missing required tool: $tool" +done +PROJECT_ID="${PROJECT_ID:-$(gcloud config get-value project 2>/dev/null)}" +[[ -n "$PROJECT_ID" && "$PROJECT_ID" != '(unset)' ]] || die 'Select the temporary Google Skills lab project.' +info "Using lab project: $PROJECT_ID" + +# The Sindri API key is supplied by the active lab panel. Never print or commit it. +if [[ -z "${SINDRI_API_KEY:-}" ]]; then + [[ -t 0 ]] || die 'Set SINDRI_API_KEY from the active lab panel (no interactive stdin).' + printf 'Paste the Sindri API key from the active lab panel (input hidden): ' >&2 + IFS= read -r -s SINDRI_API_KEY + printf '\n' >&2 +fi +[[ -n "$SINDRI_API_KEY" ]] || die 'SINDRI_API_KEY is empty.' +export SINDRI_API_KEY + +# Use the lab-provisioned bucket, never create an unrelated bucket. +GCS_BUCKET="${GCS_BUCKET:-}" +if [[ -z "$GCS_BUCKET" ]]; then + mapfile -t bucket_candidates < <( + gcloud storage buckets list --project="$PROJECT_ID" --format='value(name)' | + sed -E 's#^gs://##; s#/$##' | sed '/^$/d' + ) + if (( ${#bucket_candidates[@]} == 1 )); then + GCS_BUCKET="${bucket_candidates[0]}" + elif [[ -t 0 ]]; then + printf 'Enter the lab-provided GCS bucket name (without gs://): ' >&2 + IFS= read -r GCS_BUCKET + else + die 'Set GCS_BUCKET from the active lab panel; bucket discovery was ambiguous.' + fi +fi +GCS_BUCKET="${GCS_BUCKET#gs://}" +GCS_BUCKET="${GCS_BUCKET%/}" +[[ "$GCS_BUCKET" =~ ^[A-Za-z0-9][A-Za-z0-9._-]{1,220}$ ]] || die 'Invalid bucket name.' +gsutil ls -b "gs://$GCS_BUCKET" >/dev/null 2>&1 || die 'The lab bucket is not accessible.' +info "Using existing bucket: gs://$GCS_BUCKET" + +retry() { + local limit="$1" delay="$2" attempt + shift 2 + for ((attempt=1; attempt<=limit; attempt++)); do + if "$@"; then return 0; fi + if ((attempt < limit)); then warn "Attempt $attempt/$limit failed; retrying in ${delay}s."; sleep "$delay"; fi + done + return 1 +} +upload_artifact() { + local local_file="$1" object="$2" + [[ -s "$local_file" ]] || die "Missing or empty artifact: $local_file" + retry 3 5 gsutil -q cp "$local_file" "gs://$GCS_BUCKET/$object" || + die "Upload failed: $object" + retry 3 5 gsutil -q stat "gs://$GCS_BUCKET/$object" || + die "Cannot verify uploaded object: $object" + ok "Verified gs://$GCS_BUCKET/$object" +} + +WORKDIR="${WORKDIR:-$HOME/verifiable-credentials-codelab}" +REPO_URL='https://github.com/Sindri-Labs/verifiable-credentials-codelab.git' +info 'Task 1: install the official Sindri CLI and record its version' +if ! command -v sindri >/dev/null 2>&1; then + retry 3 5 npm install -g sindri@0.0.1-alpha.73 || + die 'Unable to install the published Sindri CLI.' +fi +sindri --version | tee "$HOME/sindri.txt" +[[ -s "$HOME/sindri.txt" ]] || die 'Sindri version file is empty.' + +info 'Task 2: clone or verify the public Sindri codelab repository' +if [[ -e "$WORKDIR" ]]; then + [[ -d "$WORKDIR/.git" ]] || die 'WORKDIR exists but is not a Git checkout; refusing to overwrite it.' + origin="$(git -C "$WORKDIR" remote get-url origin)" + [[ "$origin" == "$REPO_URL" || "$origin" == "${REPO_URL%.git}" ]] || + die 'WORKDIR has a different Git origin; refusing to modify it.' +else + git clone --depth 1 "$REPO_URL" "$WORKDIR" +fi +[[ -s "$WORKDIR/issuer/issuer.go" && -s "$WORKDIR/issuer/credential.json" ]] || + die 'Required issuer files are absent from the official checkout.' +[[ -s "$WORKDIR/verifier/bls-verify.go" ]] || + die 'Required verifier file is absent from the official checkout.' +upload_artifact "$HOME/sindri.txt" 'sindri.txt' +upload_artifact "$WORKDIR/issuer/issuer.go" 'issuer.go' +ok 'Checkpoint: Install the Sindri CLI and Clone the Source Repository (30 points)' + +info 'Task 3: issue the verifiable credential and upload both expected artifacts' +( + cd "$WORKDIR/issuer" + go run issuer.go issuer_sk.txt credential.json +) +jq -e '.credentialSubject.degreeType == "BachelorDegree" and (.proof != null)' \ + "$WORKDIR/issuer/credential-signed.json" >/dev/null || + die 'Signed credential is not valid or lacks the expected claim.' +jq -e '.Sig != null and .G2 != null and .Hm != null and .Pk != null' \ + "$WORKDIR/issuer/credential-witness-1-degreeType.json" >/dev/null || + die 'Witness JSON lacks BLS fields.' +upload_artifact "$WORKDIR/issuer/credential-signed.json" 'credential-signed.json' +upload_artifact "$WORKDIR/issuer/credential-witness-1-degreeType.json" 'credential-witness-1-degreeType.json' +ok 'Checkpoint: Issue a Verifiable Credential (20 points)' + +info 'Task 4: obtain the official public circuit and generate a presentation' +CIRCUIT_ID='42543290-0746-48d5-998a-e39197e5cf79' +if [[ -e "$WORKDIR/holder" ]]; then + [[ -s "$WORKDIR/holder/sindri.json" ]] || + die 'holder exists without a Sindri manifest; refusing to overwrite it.' +else + retry 2 8 sindri clone "$CIRCUIT_ID" "$WORKDIR/holder" || + die 'Cannot clone the public Sindri circuit with the lab API key.' +fi +( + cd "$WORKDIR/holder" + retry 2 10 sindri proof create --tag codelab \ + --input "$WORKDIR/issuer/credential-witness-1-degreeType.json" \ + > "$WORKDIR/holder/credential-proof.json" +) +jq -e '(.proof.proof | type) == "string" and + (.verification_key.verifying_key | type) == "string" and + (.public != null)' "$WORKDIR/holder/credential-proof.json" >/dev/null || + die 'Sindri response does not contain a complete proof and verification key.' +upload_artifact "$WORKDIR/holder/credential-proof.json" 'credential-proof.json' +ok 'Checkpoint: Build a Verifiable Presentation (25 points)' + +info 'Task 5: verify the proof locally using the public Go verifier' +( + cd "$WORKDIR/verifier" + go run bls-verify.go "$WORKDIR/holder/credential-proof.json" | + tee "$WORKDIR/verifier/verify_response.txt" +) +grep -Fq 'Proof verified successfully.' "$WORKDIR/verifier/verify_response.txt" || + die 'The verifier did not confirm the proof.' +upload_artifact "$WORKDIR/verifier/verify_response.txt" 'verify_response.txt' +ok 'Checkpoint: Verify the Presentation (25 points)' +info 'All four checkpoint artifacts are present. Use Check my progress in Google Skills.' +info 'static validated only; runtime/grader unverified'