Skip to content
Merged
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
7 changes: 6 additions & 1 deletion .github/workflows/deploy-pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,14 @@ jobs:
BASE_URL: /fair-data-access
run: myst build --html

- name: Copy static files (DID document, keys)
- name: Copy static files (DID documents, keys)
run: |
cp docs/did.json docs/_build/html/did.json
# Copy example consumer DID for walkthrough
if [ -d docs/example-consumer ]; then
mkdir -p docs/_build/html/example-consumer
cp docs/example-consumer/did.json docs/_build/html/example-consumer/did.json
fi
# Copy any existing wrapped keys
if [ -d docs/keys ]; then
cp -r docs/keys docs/_build/html/keys
Expand Down
63 changes: 63 additions & 0 deletions .github/workflows/walkthrough.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: Test and build walkthrough

on:
push:
branches: [main, feature/public-demo]
paths:
- 'examples/walkthrough/**'
- 'fair_data_access/**'
pull_request:
paths:
- 'examples/walkthrough/**'
- 'fair_data_access/**'

# Allow manual trigger
workflow_dispatch:

jobs:
test-and-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'

- name: Install dependencies
run: |
pip install -e .
pip install jupytext jupyter-book pandas

- name: Write private keys from secrets
working-directory: examples/walkthrough
env:
EXAMPLE_CONSUMER_PRIVATE_KEY: ${{ secrets.EXAMPLE_CONSUMER_PRIVATE_KEY }}
EXAMPLE_PROVIDER_PRIVATE_KEY: ${{ secrets.EXAMPLE_PROVIDER_PRIVATE_KEY }}
run: |
printenv EXAMPLE_CONSUMER_PRIVATE_KEY > keys/example-consumer-private.pem
printenv EXAMPLE_PROVIDER_PRIVATE_KEY > keys/example-provider-private.pem
chmod 600 keys/*-private.pem

- name: Convert .py to .ipynb and execute (smoke test)
working-directory: examples/walkthrough
run: |
for nb in 00_setup_did 01_provider 02_consumer; do
echo "=== Converting and executing ${nb}.py ==="
jupytext --to notebook "${nb}.py"
jupyter nbconvert --execute --to notebook \
--output-dir=/tmp/executed \
"${nb}.ipynb"
done

- name: Build Jupyter-book
working-directory: examples/walkthrough
run: jupyter-book build .

- name: Upload book artifact
uses: actions/upload-artifact@v4
with:
name: walkthrough-book
path: examples/walkthrough/_build/html
retention-days: 14
26 changes: 26 additions & 0 deletions docs/example-consumer/did.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"@context": [
"https://www.w3.org/ns/did/v1",
"https://w3id.org/security/suites/jws-2020/v1"
],
"id": "did:web:fair2adapt.github.io:fair-data-access:example-consumer",
"verificationMethod": [
{
"id": "did:web:fair2adapt.github.io:fair-data-access:example-consumer#key-1",
"type": "JsonWebKey2020",
"controller": "did:web:fair2adapt.github.io:fair-data-access:example-consumer",
"publicKeyJwk": {
"kty": "EC",
"crv": "P-256",
"x": "2OyUBGxbSw4NaV_-XT5qTQwZSIAdA4hFYxiUIiWOTXw",
"y": "hsn7gogSYPnx_6zWXxzMYE9nhSvJJDqdkfWHBTUbmrM"
}
}
],
"authentication": [
"did:web:fair2adapt.github.io:fair-data-access:example-consumer#key-1"
],
"assertionMethod": [
"did:web:fair2adapt.github.io:fair-data-access:example-consumer#key-1"
]
}
10 changes: 10 additions & 0 deletions examples/walkthrough/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Generated by notebooks — not committed
*.ipynb
*.enc
_build/

# Private keys (also gitignored in keys/.gitignore)
keys/*_private.pem

# Wrapped keys (generated by 01_provider, consumed by 02_consumer)
keys/wrapped-*.json
221 changes: 221 additions & 0 deletions examples/walkthrough/00_setup_did.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
# ---
# jupyter:
# jupytext:
# text_representation:
# extension: .py
# format_name: percent
# format_version: '1.3'
# jupytext_version: 1.16.0
# kernelspec:
# display_name: Python 3
# language: python
# name: python3
# ---

# %% [markdown]
# # Chapter 0 — Set up your decentralised identity (DID)
#
# Before you can request access to a protected dataset, you need a
# **decentralised identifier** — a cryptographic identity that stays with you
# across institutions.
#
# ```{admonition} One-time setup
# :class: tip
# You do this **once**. The identity you create here works for every dataset
# request you ever make through the FAIR2Adapt framework.
# ```
#
# ## What is a DID?
#
# A DID is a URL-like string that points to a small JSON document containing
# your **public key**. Anyone who knows your DID can look up your public key
# and encrypt things specifically for you.
#
# Example: `did:web:yourname.github.io:my-did:researcher`
#
# This resolves to: `https://yourname.github.io/my-did/researcher/did.json`
#
# The JSON file at that URL is your **DID document** — it contains your
# public key but **never** your private key.
#
# ## Why not just use ORCID?
#
# ORCID is great for **attribution** (who published what). But ORCID doesn't
# give you a **cryptographic key** — you can't encrypt a dataset key for an
# ORCID. DIDs add the key management layer that ORCID doesn't provide.
#
# In practice, a researcher has both: ORCID for citations, DID for data access.

# %% [markdown]
# ## Step 1 — Generate your keypair
#
# This creates an EC P-256 keypair. The private key stays on your machine.
# The public key goes into your DID document.
#
# ```{warning}
# Your **private key is your identity**. Anyone with it can impersonate you
# and decrypt data meant for you. Treat it like a password. If you lose it,
# generate a new keypair and update your DID document.
# ```

# %%
from pathlib import Path
from fair_data_access.keys import generate_did_keypair

KEYS_DIR = Path("keys")

# For this walkthrough we use the pre-generated example consumer identity.
# To create your own, uncomment the block below.

CONSUMER_PRIVATE_KEY = KEYS_DIR / "example-consumer-private.pem"
CONSUMER_PUBLIC_KEY = KEYS_DIR / "example-consumer-public.pem"

if CONSUMER_PRIVATE_KEY.exists():
print(f"Using existing keypair at {CONSUMER_PRIVATE_KEY}")
private_pem = CONSUMER_PRIVATE_KEY.read_bytes()
public_pem = CONSUMER_PUBLIC_KEY.read_bytes()
else:
# In the real workflow, you generate your keypair ONCE and store the
# private key securely (e.g. as a GitHub Secret, in a password manager,
# or on an encrypted drive). You never regenerate it — losing it means
# losing your identity and access to any data wrapped for you.
#
# For this walkthrough, the private key is stored as a GitHub Secret
# (EXAMPLE_CONSUMER_PRIVATE_KEY) and written to disk by CI before the
# notebooks run. If you're running locally for the first time, generate
# your own keypair by uncommenting the lines below:
#
# private_pem, public_pem = generate_did_keypair()
# CONSUMER_PRIVATE_KEY.write_bytes(private_pem)
# CONSUMER_PUBLIC_KEY.write_bytes(public_pem)
#
raise FileNotFoundError(
f"Consumer private key not found at {CONSUMER_PRIVATE_KEY}.\n"
"If running locally for the first time, generate a keypair:\n"
" from fair_data_access.keys import generate_did_keypair\n"
" private, public = generate_did_keypair()\n"
" Path('keys/example-consumer-private.pem').write_bytes(private)\n"
"Or set the EXAMPLE_CONSUMER_PRIVATE_KEY environment variable."
)

# %%
# Show the public key (this is safe to share — it's public by design)
print("=== Public key (safe to share) ===")
print(public_pem.decode())

# %% [markdown]
# ## Step 2 — Create your DID document
#
# The DID document is a small JSON-LD file that maps your DID string to your
# public key. It follows the [W3C DID Core](https://www.w3.org/TR/did-core/)
# specification.

# %%
import json
from fair_data_access.did import create_did_document

# For the walkthrough, we use a DID under the fair2adapt GitHub Pages domain.
# When you create your own, replace this with your domain.
CONSUMER_DID = "did:web:fair2adapt.github.io:fair-data-access:example-consumer"

did_document = create_did_document(CONSUMER_DID, public_pem)

print("=== DID Document ===")
print(json.dumps(did_document, indent=2))

# %% [markdown]
# The document above is what the world sees when they resolve your DID. It
# contains:
#
# - **`id`** — your DID string (the identifier)
# - **`verificationMethod`** — your public key in JWK format
# - **`authentication`** / **`assertionMethod`** — which key to use for
# authentication and signing
#
# Notice: **no private key anywhere in the document.**

# %% [markdown]
# ## Step 3 — Publish the DID document
#
# "Publishing" means putting the JSON file at the URL your DID resolves to.
# For `did:web:yourname.github.io:my-did:researcher`, the URL is:
#
# ```
# https://yourname.github.io/my-did/researcher/did.json
# ```
#
# ### Option A: GitHub Pages (recommended, free, 5 minutes)
#
# ```bash
# # One-time setup
# gh repo create my-did --public --description "My DID document"
# cd my-did
# mkdir -p researcher
# # Copy the DID document generated above
# cp /path/to/did.json researcher/did.json
# git add . && git commit -m "Publish DID document"
# git push
# # Enable GitHub Pages on the repo (main branch, root directory)
# gh repo edit --enable-pages --pages-branch main --pages-path /
# ```
#
# After a minute, your DID is live at
# `https://yourname.github.io/my-did/researcher/did.json`
#
# ### Option B: Institutional web server
#
# Ask your IT department to serve `did.json` at an institutional URL, e.g.
# `https://university.edu/researchers/alice/did.json`. Your DID becomes
# `did:web:university.edu:researchers:alice`.
#
# ### Option C: Personal website
#
# If you have `https://alice.example.com`, put `did.json` at the root. Your
# DID is `did:web:alice.example.com`.
#
# ---
#
# For this walkthrough, the example DID documents are already committed in
# `keys/did/` and would be served via GitHub Pages at:
# - `https://fair2adapt.github.io/fair-data-access/example-provider/did.json`
# - `https://fair2adapt.github.io/fair-data-access/example-consumer/did.json`

# %% [markdown]
# ## Step 4 — Verify your DID resolves
#
# Once published, anyone can resolve your DID and retrieve your public key.
# The `fair_data_access` library does this automatically when wrapping keys.

# %%
# For the walkthrough, we load the DID document from a local file
# instead of resolving it over the network (since the GitHub Pages
# hosting may not be set up yet for this walkthrough).

did_doc_path = KEYS_DIR / "did" / "example-consumer.json"
resolved_doc = json.loads(did_doc_path.read_text())

print("=== Resolved DID Document ===")
print(f"DID: {resolved_doc['id']}")
print(f"Key type: {resolved_doc['verificationMethod'][0]['type']}")
print(f"Curve: {resolved_doc['verificationMethod'][0]['publicKeyJwk']['crv']}")
print()
print("The provider will use the public key above to wrap the dataset")
print("key specifically for you. Only your private key can unwrap it.")

# %% [markdown]
# ## Summary
#
# You now have:
#
# | What | Where | Who can see it |
# |------|-------|----------------|
# | **Private key** | `keys/example-consumer-private.pem` (your machine, gitignored) | Only you |
# | **Public key** | `keys/example-consumer-public.pem` (committed) | Everyone |
# | **DID document** | `keys/did/example-consumer.json` (published via web) | Everyone |
# | **DID string** | `did:web:fair2adapt.github.io:fair-data-access:example-consumer` | Everyone |
#
# This identity is permanent and portable. You can use it to request access
# to **any dataset** protected by the FAIR2Adapt framework — or any other
# system that supports `did:web` resolution.
#
# **Next:** [Chapter 1 — Provider: encrypt and publish a policy](01_provider.ipynb)
Loading
Loading