Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
214 changes: 214 additions & 0 deletions labs/GSP1302/HASHNODE.md
Original file line number Diff line number Diff line change
@@ -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.
156 changes: 156 additions & 0 deletions labs/GSP1302/lab.sh
Original file line number Diff line number Diff line change
@@ -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'
Loading