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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Secure decentralized workspace built over the [Named Data Networking](https://named-data.net) stack.

[![ci](https://github.com/pulsejet/ownly/actions/workflows/ci.yml/badge.svg)](https://github.com/pulsejet/ownly/actions/workflows/ci.yml)
[![ci](https://github.com/UCLA-IRL/ownly/actions/workflows/ci.yml/badge.svg)](https://github.com/UCLA-IRL/ownly/actions/workflows/ci.yml)
[![license](https://img.shields.io/badge/license-MIT-blue)](./LICENSE)
[![Website](https://img.shields.io/website?url=https%3A%2F%2Fownly.work)](https://ownly.work)

Expand Down
462 changes: 462 additions & 0 deletions docs/history-verification.md

Large diffs are not rendered by default.

62 changes: 55 additions & 7 deletions ndn/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import (
"crypto/cipher"
"fmt"
"runtime"
"sync"
"syscall/js"
"time"

"github.com/UCLA-IRL/ownly/ndn/historylog"
enc "github.com/named-data/ndnd/std/encoding"
"github.com/named-data/ndnd/std/log"
"github.com/named-data/ndnd/std/ndn"
Expand All @@ -30,6 +32,13 @@ type App struct {
store ndn.Store
keychain ndn.KeyChain

// The browser's durable capture and sender are bound to this configuration.
// Empty leaves capture disabled for the consumer-only Node app.
historyLogID string
historyOutbox historylog.Outbox
historyMu sync.Mutex
historySenders map[string]*historySender

// Trust config for testbed certs only
// In practice all trust configs are currently the same, but
// each workspace could theoretically have a different trust config.
Expand Down Expand Up @@ -83,12 +92,18 @@ func NewApp() *App {
}

a := &App{
store: store,
keychain: kc,
dskReqs: make(map[string]*time.Timer),
bootSyncs: make(map[string]bool),
joinPayloads: make(map[string][]byte),
store: store,
keychain: kc,
historyLogID: historyLogScopeID,
historySenders: make(map[string]*historySender),
dskReqs: make(map[string]*time.Timer),
bootSyncs: make(map[string]bool),
joinPayloads: make(map[string][]byte),
}
a.historyOutbox = historylog.NewJSOutbox(_ndnd_store_js, func(scope historylog.Scope) {
store.ClearCache()
a.wakeHistorySender(scope)
})
a.initialize()
return a
}
Expand Down Expand Up @@ -134,6 +149,38 @@ func (a *App) initialize() {
}
}

// historyCapture binds an operation to its original identity and configured log.
// The browser enables capture through its IndexedDB-backed outbox. A headless
// producer has no durable outbox and therefore leaves capture disabled.
func (a *App) historyCapture(identity enc.Name) (*historylog.Capture, error) {
if a.historyLogID == "" {
return nil, nil
}
scope, err := historylog.NewScope(identity, a.historyLogID)
if err != nil {
return nil, err
}
if err = a.ensureHistorySender(identity, scope); err != nil {
log.Warn(a, "History sender is not ready; durable capture will recover later", "err", err)
}
return historylog.NewCapture(a.historyOutbox, scope)
}

func (a *App) persistIdentityHistory(signer ndn.Signer, wires ...enc.Wire) error {
if a.historyLogID == "" {
return nil
}
identity, err := security.GetIdentityFromKeyName(signer.KeyName())
if err != nil {
return err
}
capture, err := a.historyCapture(identity)
if err != nil {
return err
}
return capture.PersistProducedData(wires...)
}

func (a *App) String() string {
return "app"
}
Expand Down Expand Up @@ -234,9 +281,10 @@ func (a *App) JsApi() js.Value {
return a.IsWorkspaceOwner(p[0].String())
}),

// get_workspace(name: string, ignore: boolean): Promise<WorkspaceAPI>;
// get_workspace(name: string, allowMissingHistory?: boolean): Promise<WorkspaceAPI>;
"get_workspace": jsutil.AsyncFunc(func(this js.Value, p []js.Value) (any, error) {
return a.GetWorkspace(p[0].String(), p[1].Bool())
allowMissingHistory := len(p) > 1 && p[1].Truthy()
return a.GetWorkspace(p[0].String(), allowMissingHistory)
}),

// wait_user_key(wksp: string): Promise<void>;
Expand Down
20 changes: 10 additions & 10 deletions ndn/app/boot_sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (

"syscall/js"

"github.com/UCLA-IRL/ownly/ndn/app/tlv"
"github.com/UCLA-IRL/ownly/ndn/historylog"
enc "github.com/named-data/ndnd/std/encoding"
"github.com/named-data/ndnd/std/log"
"github.com/named-data/ndnd/std/ndn"
Expand All @@ -17,7 +19,6 @@ import (
ndn_sync "github.com/named-data/ndnd/std/sync"
"github.com/named-data/ndnd/std/types/optional"
jsutil "github.com/named-data/ndnd/std/utils/js"
"github.com/pulsejet/ownly/ndn/app/tlv"
)

type bootSyncSession struct {
Expand All @@ -30,9 +31,8 @@ func (a *App) NewBootSyncAlo(client ndn.Client, nodeName, group enc.Name, initia
Name: nodeName,
InitialState: initialState,
Svs: ndn_sync.SvSyncOpts{
Client: client,
GroupPrefix: group,
IgnoreValidity: optional.Some(false),
Client: client,
GroupPrefix: group,
},
// Data in boot sync is small; no snapshot needed.
Snapshot: nil,
Expand Down Expand Up @@ -463,9 +463,9 @@ func (a *App) ownerSub(client ndn.Client, wkspName enc.Name, rootSigner ndn.Sign
}, 1)
client.ValidateExt(ndn.ValidateExtArgs{
Data: args.Data,
RawData: args.RawData,
SigCovered: args.SigCovered,
UseDataNameFwHint: optional.Some(true),
IgnoreValidity: optional.Some(false),
Callback: func(valid bool, err error) {
validCh <- struct {
valid bool
Expand Down Expand Up @@ -503,13 +503,13 @@ func (a *App) ownerSub(client ndn.Client, wkspName enc.Name, rootSigner ndn.Sign
return
}

if err := a.keychain.InsertCert(userCert.Join()); err != nil {
log.Warn(a, "Failed to store final cert locally", "err", err, "name", userCertData.Name())
}

// Keep track of user certs issued by this owner
if err := client.Store().Put(userCertData.Name(), userCert.Join()); err != nil {
if err := historylog.PersistProducedData(client, userCert); err != nil {
log.Warn(a, "Failed to store final cert in local store", "err", err, "name", userCertData.Name())
return
}
if err := a.keychain.InsertCert(userCert.Join()); err != nil {
log.Warn(a, "Failed to store final cert locally", "err", err, "name", userCertData.Name())
}

// Index the final cert as a peer so the Authenticated Peers
Expand Down
2 changes: 1 addition & 1 deletion ndn/app/encrypt.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ import (
"fmt"
"time"

"github.com/UCLA-IRL/ownly/ndn/app/tlv"
spec_repo "github.com/named-data/ndnd/repo/tlv"
enc "github.com/named-data/ndnd/std/encoding"
"github.com/named-data/ndnd/std/log"
"github.com/named-data/ndnd/std/ndn"
spec "github.com/named-data/ndnd/std/ndn/spec_2022"
"github.com/named-data/ndnd/std/types/optional"
"github.com/pulsejet/ownly/ndn/app/tlv"
)

const (
Expand Down
99 changes: 99 additions & 0 deletions ndn/app/history_reader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
//go:build js && wasm

package app

import (
"crypto/sha256"
"fmt"

enc "github.com/named-data/ndnd/std/encoding"
"github.com/named-data/ndnd/std/merklelog"
"github.com/named-data/ndnd/std/ndn"
merkledef "github.com/named-data/ndnd/std/ndn/merklelog"
"github.com/named-data/ndnd/std/object"
"github.com/named-data/ndnd/std/security"
)

// The endpoint is routing configuration. Response authorization and append
// signing are defined by schema.trust.
var historyLogName, _ = enc.NameFromStr("/ndn/edu/ucla/ndnd/merkle")

// historyLogScopeID binds durable rows to the endpoint and LVS signer policy.
// A future signer or endpoint change must use a new ID, never retarget rows.
const historyLogScopeID = "ucla-ndnd-merkle/site-issued-v1"

// historyReader returns a client for retained workspace Data.
func (a *App) historyReader(client ndn.Client, requester enc.Name, allowMissingHistory bool) (ndn.Client, error) {
if len(historyLogName) == 0 {
return client, nil
}

proofClient, err := a.historyLogClient(requester)
if err != nil {
return nil, err
}
policy := merklelog.NewCertExpiredPolicy(proofClient)
if allowMissingHistory {
policy = allowMissingHistoryPolicy(proofClient, policy)
}
return withHistoryExpiry(client, policy), nil
}

// Magic only waives expiry after an authenticated NotFound response.
// Included packets still use the SDK's ingestion-time policy.
func allowMissingHistoryPolicy(proofClient *merklelog.Client, historyPolicy ndn.CertExpiredCallback) ndn.CertExpiredCallback {
return func(args ndn.CertExpiredCallbackArgs, complete func(error)) {
if args.Data == nil || args.RawData.Length() == 0 {
historyPolicy(args, complete)
return
}
hash := sha256.Sum256(args.RawData.Join())
proofClient.Check(hash[:], func(response *merkledef.CheckResponse, err error) {
if err != nil {
complete(fmt.Errorf("Merkle log check failed: %w", err))
return
}
if response.Result.Status == merkledef.CheckStatusNotFound {
security.IgnoreExpiredCert(args, complete)
return
}
// A second check keeps proof and ingestion-time handling in the SDK.
historyPolicy(args, complete)
})
}
}

// historyLogClient constructs a client whose requests and responses use ordinary
// current-validity trust. Historical expiry is only applied by historyReader.
func (a *App) historyLogClient(requester enc.Name) (*merklelog.Client, error) {
// The log client uses the application's unmodified SchemaBytes trust setup.
// It deliberately has no history policy: proof responses require a currently
// valid signer and cannot be justified by another proof.
trust, err := getTrustConfig(a.keychain)
if err != nil {
return nil, err
}
return merklelog.NewClient(
object.NewClient(a.engine, a.store, trust), historyLogName, requester,
)
}

// historyExpiryClient adds the history policy only to object consumption.
// Direct validation, live SVS, and command traffic retain current-validity checks.
type historyExpiryClient struct {
ndn.Client
onCertExpired ndn.CertExpiredCallback
}

func withHistoryExpiry(client ndn.Client, onCertExpired ndn.CertExpiredCallback) ndn.Client {
return &historyExpiryClient{Client: client, onCertExpired: onCertExpired}
}

func (c *historyExpiryClient) Consume(name enc.Name, callback func(ndn.ConsumeState)) {
c.ConsumeExt(ndn.ConsumeExtArgs{Name: name, Callback: callback})
}

func (c *historyExpiryClient) ConsumeExt(args ndn.ConsumeExtArgs) {
args.OnCertExpired = c.onCertExpired
c.Client.ConsumeExt(args)
}
Loading
Loading