A community based topic aggregation platform built on atproto
1#!/bin/bash
2# Derive public key from existing PDS_ROTATION_KEY and create did.json
3#
4# This script takes your existing private key and derives the public key from it.
5# Use this if you already have a PDS running with a rotation key but need to
6# create/fix the did.json file.
7#
8# Usage: ./scripts/derive-did-from-key.sh
9
10set -e
11
12SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
13PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
14OUTPUT_DIR="$PROJECT_DIR/static/.well-known"
15
16# Colors
17GREEN='\033[0;32m'
18YELLOW='\033[1;33m'
19RED='\033[0;31m'
20NC='\033[0m'
21
22log() { echo -e "${GREEN}[DERIVE]${NC} $1"; }
23warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
24error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; }
25
26# Check for required tools
27if ! command -v openssl &> /dev/null; then
28 error "openssl is required but not installed"
29fi
30
31if ! command -v python3 &> /dev/null; then
32 error "python3 is required for base58 encoding"
33fi
34
35# Check for base58 library
36if ! python3 -c "import base58" 2>/dev/null; then
37 warn "Installing base58 Python library..."
38 pip3 install base58 || error "Failed to install base58. Run: pip3 install base58"
39fi
40
41# Load environment to get the existing key
42if [ -f "$PROJECT_DIR/.env.prod" ]; then
43 source "$PROJECT_DIR/.env.prod"
44elif [ -f "$PROJECT_DIR/.env" ]; then
45 source "$PROJECT_DIR/.env"
46else
47 error "No .env.prod or .env file found"
48fi
49
50if [ -z "$PDS_ROTATION_KEY" ]; then
51 error "PDS_ROTATION_KEY not found in environment"
52fi
53
54# Validate key format (should be 64 hex chars)
55if [[ ! "$PDS_ROTATION_KEY" =~ ^[0-9a-fA-F]{64}$ ]]; then
56 error "PDS_ROTATION_KEY is not a valid 64-character hex string"
57fi
58
59log "Deriving public key from existing PDS_ROTATION_KEY..."
60
61# Create a temporary PEM file from the hex private key
62TEMP_DIR=$(mktemp -d)
63PRIVATE_KEY_HEX="$PDS_ROTATION_KEY"
64
65# Convert hex private key to PEM format
66# secp256k1 curve OID: 1.3.132.0.10
67python3 > "$TEMP_DIR/private.pem" << EOF
68import binascii
69
70# Private key in hex
71priv_hex = "$PRIVATE_KEY_HEX"
72priv_bytes = binascii.unhexlify(priv_hex)
73
74# secp256k1 OID
75oid = bytes([0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x0a])
76
77# Build the EC private key structure
78# SEQUENCE { version INTEGER, privateKey OCTET STRING, [0] OID, [1] publicKey }
79# We'll use a simpler approach: just the private key with curve params
80
81# EC PARAMETERS for secp256k1
82ec_params = bytes([
83 0x30, 0x07, # SEQUENCE, 7 bytes
84 0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x0a # OID for secp256k1
85])
86
87# EC PRIVATE KEY structure
88# SEQUENCE { version, privateKey, [0] parameters }
89inner = bytes([0x02, 0x01, 0x01]) # version = 1
90inner += bytes([0x04, 0x20]) + priv_bytes # OCTET STRING with 32-byte key
91inner += bytes([0xa0, 0x07]) + bytes([0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x0a]) # [0] OID
92
93# Wrap in SEQUENCE
94key_der = bytes([0x30, len(inner)]) + inner
95
96# Base64 encode
97import base64
98key_b64 = base64.b64encode(key_der).decode('ascii')
99
100# Format as PEM
101print("-----BEGIN EC PRIVATE KEY-----")
102for i in range(0, len(key_b64), 64):
103 print(key_b64[i:i+64])
104print("-----END EC PRIVATE KEY-----")
105EOF
106
107# Extract the compressed public key
108PUBLIC_KEY_HEX=$(openssl ec -in "$TEMP_DIR/private.pem" -pubout -conv_form compressed -outform DER 2>/dev/null | \
109 tail -c 33 | xxd -p | tr -d '\n')
110
111# Clean up
112rm -rf "$TEMP_DIR"
113
114if [ -z "$PUBLIC_KEY_HEX" ] || [ ${#PUBLIC_KEY_HEX} -ne 66 ]; then
115 error "Failed to derive public key. Got: $PUBLIC_KEY_HEX"
116fi
117
118log "Derived public key: ${PUBLIC_KEY_HEX:0:8}...${PUBLIC_KEY_HEX: -8}"
119
120# Encode public key as multibase with multicodec
121PUBLIC_KEY_MULTIBASE=$(python3 << EOF
122import base58
123
124# Compressed public key bytes
125pub_hex = "$PUBLIC_KEY_HEX"
126pub_bytes = bytes.fromhex(pub_hex)
127
128# Prepend multicodec 0xe7 for secp256k1-pub
129# 0xe7 as varint is just 0xe7 (single byte, < 128)
130multicodec = bytes([0xe7, 0x01]) # 0xe701 for secp256k1-pub compressed
131key_with_codec = multicodec + pub_bytes
132
133# Base58btc encode
134encoded = base58.b58encode(key_with_codec).decode('ascii')
135
136# Add 'z' prefix for multibase
137print('z' + encoded)
138EOF
139)
140
141log "Public key multibase: $PUBLIC_KEY_MULTIBASE"
142
143# Generate the did.json file
144log "Generating did.json..."
145
146mkdir -p "$OUTPUT_DIR"
147
148cat > "$OUTPUT_DIR/did.json" << EOF
149{
150 "id": "did:web:coves.social",
151 "alsoKnownAs": ["at://coves.social"],
152 "verificationMethod": [
153 {
154 "id": "did:web:coves.social#atproto",
155 "type": "Multikey",
156 "controller": "did:web:coves.social",
157 "publicKeyMultibase": "$PUBLIC_KEY_MULTIBASE"
158 }
159 ],
160 "service": [
161 {
162 "id": "#atproto_pds",
163 "type": "AtprotoPersonalDataServer",
164 "serviceEndpoint": "https://coves.me"
165 }
166 ]
167}
168EOF
169
170log "Created: $OUTPUT_DIR/did.json"
171echo ""
172echo "============================================"
173echo " DID Document Generated Successfully!"
174echo "============================================"
175echo ""
176echo "Public key multibase: $PUBLIC_KEY_MULTIBASE"
177echo ""
178echo "Next steps:"
179echo " 1. Copy this file to your production server:"
180echo " scp $OUTPUT_DIR/did.json user@server:/opt/coves/static/.well-known/"
181echo ""
182echo " 2. Or if running on production, restart Caddy:"
183echo " docker compose -f docker-compose.prod.yml restart caddy"
184echo ""
185echo " 3. Verify it's accessible:"
186echo " curl https://coves.social/.well-known/did.json"
187echo ""