Skip to content

Feature: Add cryptographic signatures to chat messages #18

Open

Description

@melvincarvalho

Summary

Add support for cryptographically signing chat messages using Schnorr signatures (secp256k1), aligned with the live SolidOS solid-ui implementation.

Reference Implementation (Live in SolidOS)

SolidOS solid-ui already implements this:

How It Works (Live Implementation)

Signing a Message (chatLogic.js)

import { signMsg, SEC } from './signature'
import { getPrivateKey } from './keys'

// When sending a message:
const privateKey = await getPrivateKey(me)
const sig = signMsg(msg, privateKey)
sts.push($rdf.st(message, $rdf.sym(`${SEC}proofValue`), $rdf.lit(sig), chatDocument))

Verifying a Message (message.js)

import { verifySignature, SEC } from './signature'
import { getPublicKey } from './keys'

// When rendering a message:
const signature = store.any(msgId, $rdf.sym(`${SEC}proofValue`))

if (!signature?.value) {
  unsignedMessage = true
  debug.warn(msgId.uri + ' is unsigned')
} else {
  getPublicKey(creator).then(publicKey => {
    if (!verifySignature(signature?.value, msg, publicKey)) {
      debug.warn('invalid signature')
    }
  })
}

// UI: unsigned messages get red background
if (unsignedMessage) messageRow.setAttribute('style', 'background-color: red')

RDF Storage

@prefix sec: <https://w3id.org/security#> .

# Signature stored directly on message
<#msg-1234567890>
    a wf:Message ;
    sioc:content "Hello, world!" ;
    dct:created "2026-01-11T12:00:00Z" ;
    foaf:maker <https://melvincarvalho.com/#me> ;
    sec:proofValue "e3b0c44298fc1c149afbf4c8996fb924..." .  # Schnorr signature hex

Message Object for Signing

const msg = {
  id: msgId.uri,                    // Message URI
  created: '2026-01-11T12:00:00Z',  // dct:created
  content: 'Hello, world!',         // sioc:content  
  maker: 'https://melvincarvalho.com/#me'  // foaf:maker
}

// Sign: JSON.stringify(msg) → SHA-256 → Schnorr sign
const sig = signMsg(msg, privateKey)  // Returns hex string

Key Storage in Pod

# Public key (world-readable)
<#me> solid:publicKey "a1b2c3d4..." .  # 64 char hex

# Private key (owner-only ACL)
<#me> solid:privateKey "f1e2d3c4..." .  # 64 char hex

Cryptography

  • Algorithm: Schnorr signatures on secp256k1 curve
  • Library: @noble/curves/secp256k1 + @noble/hashes/sha256
  • Process:
    1. Serialize message to JSON
    2. SHA-256 hash the JSON
    3. Schnorr sign the hash
    4. Store as sec:proofValue hex string

Security Considerations

Risks of Storing Private Keys in Pods

Risk Description Severity
Server admin access Pod operators could read private keys Medium
ACL misconfiguration Wrong permissions could expose keys High
No encryption at rest Keys stored as plaintext Medium
Pod compromise Hacked pod = compromised keys High

Mitigations

  • ACL restricts private key to owner-only
  • Dedicated /settings/keys/ container with strict ACL
  • Key rotation support if compromised

Browser Extensions for Enhanced Security

For users who don't want private keys in their pod:

  • Podkey - Browser extension for Solid key management
    • Keeps private keys in browser extension storage
    • Signs messages without exposing keys to pod server
    • Intercepts signing requests from Solid apps

Implementation Tasks for solid-chat/app

  • Add @noble/curves and @noble/hashes dependencies
  • Port key functions from solid-ui keys.ts
  • Port sign/verify from solid-ui signature.ts
  • Add sec:proofValue triple when sending messages
  • Verify signatures when rendering messages
  • Add UI indicator (red background for unsigned, green check for verified)
  • Handle unsigned messages gracefully (backwards compat)
  • Document Podkey extension integration

Dependencies

{
  "@noble/curves": "^1.2.0",
  "@noble/hashes": "^1.3.0"
}

References

Activity

  1. melvincarvalho commented on Jan 11, 2026

    @melvincarvalho
    ContributorAuthor

    Analysis of SolidOS Chat Signature System

    Strengths ✅

    1. Simplicity

    • Clean integration - just one extra triple (sec:proofValue)
    • Reuses existing message fields for signing (no duplication)
    • Minimal dependencies (@noble/curves is well-audited)

    2. Standard-ish Approach

    • Uses https://w3id.org/security# vocabulary
    • Schnorr on secp256k1 aligns with Bitcoin/Nostr ecosystem
    • JSON serialization is predictable

    3. Backwards Compatible

    • Unsigned messages still work (just get red background)
    • Gradual adoption possible

    Weaknesses ⚠️

    1. Signing Object is Fragile

    msg = { id, created, content, maker }
    • What if fields are added later? Signature breaks
    • No versioning or canonicalization
    • Order-dependent JSON serialization could cause issues across implementations

    2. Private Key Storage is Problematic

    <#me> solid:privateKey "HEX_KEY" .
    • Stored as plaintext literal in pod
    • Pod server admin can read it
    • If ACL wrong, key leaks publicly
    • No encryption at rest
    • No hardware key support

    3. No Key Binding to WebID

    • Public key stored separately from WebID profile
    • No cryptographic proof that key belongs to that WebID
    • Someone could claim any public key

    4. Verification is Fire-and-Forget

    getPublicKey(creator).then(publicKey => {
      if (!verifySignature(...)) debug.warn('invalid signature')
    })
    • Only logs warning, doesn't update UI for invalid signatures
    • No distinction between "unsigned" vs "invalid signature" (both could be attacks)
    • Async verification means UI might render before verification completes

    5. No Replay/Timestamp Protection

    • Same message content + timestamp = same signature
    • No nonce or sequence number
    • Could potentially replay old messages

    6. Red Background UX

    • Punishes users who haven't set up keys
    • New users see all their messages in red
    • Doesn't distinguish "no key setup" from "tampered message"

    Security Concerns 🔒

    Issue Risk Impact
    Plaintext private keys High Full impersonation if pod compromised
    No key rotation protocol Medium Can't recover from key compromise
    Mutable message fields Medium Edit message → signature invalid → looks tampered
    No revocation Medium Can't invalidate compromised keys
    JSON serialization order Low Cross-implementation incompatibility

    Recommendations for solid-chat/app

    1. Improve Signing Object

    // Add version and sort keys for consistency
    const msg = {
      v: 1,  // Version for future changes
      content,
      created,
      id,
      maker
    }
    const canonical = JSON.stringify(msg, Object.keys(msg).sort())

    2. Better UI States

    🔒 Verified (green) - signature valid
    ⚠️ Unsigned (grey) - no signature present  
    ❌ Invalid (red) - signature failed verification
    🔑 No key (blue) - author has no public key
    

    3. Consider Podkey Integration First

    • Don't store private keys in pod
    • Use browser extension for signing
    • Pod only stores public key

    4. Add Key Binding

    # In WebID profile, cryptographically bind key
    <#me> sec:assertionMethod <#key1> .
    <#key1> a sec:JsonWebKey2020 ;
        sec:controller <#me> ;
        sec:publicKeyHex "..." .

    5. Handle Edited Messages

    • Re-sign on edit, or
    • Store original signature + edit history, or
    • Mark edited messages differently

    Overall Rating: 6/10

    Good MVP approach, gets the job done, but has security gaps that matter for a decentralized system where you can't trust the server. The Podkey extension approach is the right direction for production use.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions