Skip to content

Correxit Security Model

Correxit is a serverless, frontend-only JupyterLab extension. All logic runs in the browser. There is no backend authority or trusted third party. Cryptographic primitives use window.crypto and openpgp.js.

Threat Profile

  • Student reads the reference cells: secret reference encryption (openpgp, AES-256).
  • Student reads answerable payload: answer payloads are SHA-256 digests.
  • Student reads the roster: roster encryption (openpgp, AES-256).
  • Student alters authored assignment state: assignment MAC and authenticated issued contents (keyed SHA-256).
  • Student edits cells after submission: workbook locking and freezing.
  • Peer reads answers from file: sealed submissions to the author key.
  • Student tampers after submit: seal hash plus transport integrity.
  • Student copies peer's sealed blobs: assignee and cell id are bound into the encrypted payload.
  • Student starts from a forged blank slate: issue digest plus issuer PGP signature.

Out of scope: malicious authors, browser memory extraction, compromised JupyterLab servers, and preventing cross-student file access itself. Those are trust, host, or LMS access-control problems, not workbook-format problems. If a peer does obtain another student's file, Correxit still aims to protect the contents it actually encrypts or seals: secret references, the roster, and sealed submission sources.

Key Management

Plaintext keys are never written to notebook metadata, settings JSON, or assignment files. They enter via user input or a configured Unlocker. Rubric.Unlocked carries the symmetric key; Rubric.Locked has key: null. Notebook metadata stores only locked rubrics.

The shipped secrets-manager connector is in-memory. Deployments may configure a different secrets connector or Unlocker; that is an intentional key custody choice made by the deployment, not a hidden Correxit write path.

The Unlocker also acquires credentials for creation, submission, revision, and forensic recovery. A hosted provider can supply generated keys directly, without user passphrases. Author keys and student revision keys have separate purposes: student credentials must never unlock the rubric or enter the author cache. Hosted stores must authorize access independently of notebook-supplied identifiers and scope student credentials by rubric and assignee. Creation awaits author-key storage; a hosted provider must persist student credentials before allowing submission to proceed.

The author's PGP private key is stored in assignment.keys.private.author, encrypted with the same symmetric key that encrypts the roster. unlock recovers it into local scope, uses it, and discards it before returning.

Settings Secrets

Provider-dispatched plugins may receive API tokens via the JupyterLab settings editor. dispatcher.ts registers a compose transform that intercepts token values, moves them to SecretsManager, blanks them from persisted JSON, and re-injects them into composite settings on later loads.

Integrity

Rubric MAC

Authenticates the mutable author-controlled grading state: rubric.cxtformat, rubric.id, rubric cells, rubric references, format-2 rubric.contents, assignment assignee, expiration, id, issue, issuer, keys (author components only), name, overdue, penalty, resources, report (interventions + scores, sorted), and roster.

The Rubric.terms function extracts Keys.author(keys) to include only { private, public } for the author. Student key fields (keys.private.assignee, keys.public.assignee) are absent from Rubric.Terms, so they do not affect the HMAC. A student setting their own keypair at submit time does not invalidate the MAC.

mac = HMAC-SHA-256(JSON.stringify(Rubric.terms(rubric)), key)

Any modification to authenticated fields invalidates the MAC.

This is different from Workbook.Identifier. Identifier is a small routing key. The MAC proves that the broader authored assignment state still matches the secret key held by the author side of Correxit.

Authenticated: rubric.cxtformat, rubric.id, cells, references, format-2 contents, assignee, expiration, id, issue, issuer, keys (author only), name, overdue, penalty, resources, report, roster.

Not authenticated: certification, collected, distribution, seal, submission, submitted. These change after signing or are set by the student (who does not have the symmetric key).

Issued Contents

Format 2 records an ordered contents array when issuing a workbook. Each entry contains { id, type, digest }. The digest is null for a rubric cell, whose source is the student's answer. For every other issued cell, including visible references, secret references, instructions, and setup code, it is:

digest = HMAC-SHA-256(JSON.stringify([rubric.id, id, type, source]), key)

Secret references are represented by their decrypted source and code type. Re-encryption therefore does not change their commitment. Keyed commitments avoid exposing plaintext hashes of easily guessed secret sources. The entire array is authenticated by the rubric MAC and included in the issue digest. Unissued author templates use contents: null.

Issuance and authentication require unique, non-empty string IDs on every notebook cell, including added working cells. Invalid IDs are rejected before building or checking the manifest; they are never coerced into identities.

Rubric cells, references, scores, and interventions use own entries when looking up IDs. Graded cells and references reject __proto__ because JupyterLab's metadata copier cannot preserve that entry.

Grading and collection require the issued cells to retain their identities, types, relative order, and fixed sources. Students may edit rubric cell sources and add working cells; added cells are not authenticated instructor content. Missing or duplicated issued cells, substituted ciphertexts, altered visible references, and changed setup code fail authentication.

Unlock prepares all decrypted replacements, authenticates the prospective plaintext notebook, and only then writes them. An assigned workbook cannot silently repair away missing cells or references. Forensic recovery is explicit and does not confer grading authority.

Symmetric decryption rejects malformed PGP messages. A plaintext reference is accepted only when it matches its authenticated commitment, supporting an author-side save of an unlocked workbook without accepting substituted tests.

Grade Provenance

Parsing metadata is not authentication. Scanning reads notebook identity and workflow metadata without acquiring credentials, and cannot produce trusted scores. Batch grading authenticates the rubric and issued contents before either executing cells or reusing a stored report, including reports awaiting manual review. Certification alone is an unsigned workflow marker and cannot skip an ungraded submission.

Student and unassigned author previews can return unverified scores for local feedback. They cannot supply certified grades, CSV scores, or collection.

Workbook.Grade discriminates in-memory results with verified: true | false. Workbook.Certified requires a verified grade. This provenance is never read from notebook metadata. CSV exports use only verified command results; scanned reports and failed authentication produce blank points, possible points, certification, and collection fields, with resolved=false. To export existing certified scores, run batch grading first; authenticated cached reports do not re-execute the notebook. Collection independently authenticates before calling the configured collector.

Submission Time and Late Policy

Correxit can record two different kinds of submission evidence:

  • submission: a local timestamp written into the workbook when the student clicks submit
  • submitted: an optional external receipt returned by a Submitter plugin

Only the second can come from an authority outside the document.

The local submission field is intentionally not authenticated. In a frontend-only system there is no trusted Correxit clock, and there is no backend authority that can witness when the student clicked submit. This means the local timestamp is useful workflow state, but not cryptographic proof of when delivery happened.

That distinction matters for overdue handling:

  • In backendless workflows, Correxit may still apply a local overdue policy to the document being graded. This is a convenience policy for manual exchange workflows such as git, email, or shared storage. Corrector may also choose to accept only workbooks with a non-null local submission timestamp.
  • In backend-backed workflows, the upstream system's deadline rules and receipt should be treated as authoritative. Correxit should not be described as the authority on timeliness in that mode.

Put differently: Correxit can protect workbook contents and preserve local submission state, but it cannot by itself prove that a submission was on time.

Blank-Slate Authenticity

Each propagated workbook carries two additional assignment fields:

  • issue: a deterministic digest of the issued blank-slate state
  • issuer: a cleartext PGP signature over that digest made with the author key

These fields stay stable after later author-side edits. The distribution path can recompute the whole blank-slate digest before handing it to a student. Grading cannot recompute that whole digest after answers change. It verifies the rubric MAC and the separate format-2 contents commitments instead; checking the stored issue signature alone is not proof of current reference sources.

Seal Integrity

Sealed submissions use a separate integrity mechanism. At submit time, the SHA-256 hash of all ciphertexts, sorted by cell id and joined with newline, is stored as assignment.seal. At unlock time, Workbook.unlock recomputes the hash from the current ciphertexts. A mismatch fails the workbook.

The seal hash is not signed. It cannot be: at submit time the workbook is locked and the HMAC key is absent. The hash detects accidental corruption and casual tampering. It does not stop a sophisticated student from re-encrypting cells with the cleartext public key, recomputing the hash, and updating assignment.seal. Strong post-submission integrity against deliberate file modification depends on the Submitter plugin delivering an independent copy to the LMS at submit time.

Transport Integrity (Plugin Responsibility)

The distributor does not return a receipt. Correxit records only the local assignment.distribution timestamp after a successful delivery and save. Blank-slate authenticity comes from issue and issuer, not from distribution. If an LMS needs a delivery receipt, that record belongs to the external system.

Encryption

Symmetric (AES-256 via openpgp, password-based)

  • Reference cells: comparable and correctable cells marked secret: true have their reference cells encrypted with the assignment key on lock. Students cannot read secret reference answers without the key. Cells marked secret: false leave reference cells visible.
  • Roster: Encrypted with the assignment key on lock. Students cannot enumerate the roster.
  • Answerable cells: Store a SHA-256 digest of the expected output, not the output itself. One-way.
  • Author PGP private key: Encrypted with the author key at authoring time. Stored in assignment.keys.private.author.
  • Student PGP private key: Encrypted with the student's own key (derived from their passphrase or supplied by the Unlocker). Stored in assignment.keys.private.assignee. Null for fire-and-forget submissions.

Asymmetric (PGP, Curve25519)

  • Sealed submissions: At submit time, each rubric cell's source is encrypted as JSON.stringify({ assignee, id, source, type }) using security.seal with the author's PGP public key (and optionally the student's). The cell is converted to raw type with editable: false and source_hidden: true.

At grading time, Workbook.unlock decrypts the author's PGP private key, verifies the seal hash, and unseals each cell. Payload binding prevents cross-student replay (assignee) and cell rearrangement (id).

Sealed Submissions

Lifecycle

  1. Authoring (Workbook.convert): security.keypair() generates a Curve25519 PGP keypair. The public key is stored in cleartext. The private key is encrypted with the supplied or derived author key before storage.

  2. Propagation (propagator.ts): No changes. Keys travel in assignment metadata, copied to each student notebook. The seal and student key fields start as null.

  3. Submit, fire-and-forget (Workbook.submit): Each rubric cell is sealed to [keys.public.author]. Cells are sorted by id. The seal hash is computed and stored. The workbook is frozen.

  4. Submit, with revision access (commands.ts: submit): The Unlocker supplies student credentials, prompting for a passphrase by default. Correxit generates a student keypair and encrypts its private key with the supplied or derived key. Cells are sealed to both [keys.public.author, keys.public.assignee].

  5. Revise (commands.ts: revise, Workbook.revise): The Unlocker acquires student credentials to decrypt their PGP private key, unseals all cells, clears seal, submission, submitted, and student key fields, then defrosts the notebook.

  6. Grading (Workbook.unlock): Authenticates metadata first. If assignment.seal is non-null, it decrypts the author PGP private key in local scope, verifies the seal hash, and prepares unsealed rubric cells. It also prepares decrypted references and authenticates the complete prospective contents before writing any replacement. Only then is seal cleared.

Missing issued cells or references fail assigned format-2 unlock in both live and headless notebooks. Workbook.recover remains the explicit forensic escape hatch for damaged workbooks. Student revision can still recover remaining cells in a live notebook, but that repair is not grading or collection authority.

Payload Binding

Each sealed cell payload is { assignee, id, source, type }. At unseal time:

  • assignee must match assignment.assignee. Prevents Student B from copying Student A's ciphertexts.
  • id must match the cell id in the notebook. Prevents cell rearrangement after submission.
  • type restores the original cell type (e.g. code, markdown) from raw.

Cross-assignment replay is not a concern: each rubric gets a fresh random PGP keypair at authoring time, so ciphertext from one assignment cannot be decrypted by another assignment's key.

Post-Grading Plaintext

Graded notebooks are saved with student answers decrypted. The seal protects the submission-to-grading corridor. After certification, the notebook is a feedback receipt, so students need plaintext to review scores and outputs. Re-encrypting after grading would lock fire-and-forget students out of their own feedback for no security gain.

PGP Nondeterminism

PGP encryption uses random session keys. Encrypting the same plaintext twice produces different ciphertext. The seal hash can only be verified by hashing the existing ciphertexts, never by re-encrypting and comparing.

Lifecycle

Assignment Fields

  • assignee: string, signed, student identifier.
  • roster: string[], signed, encrypted on lock.
  • expiration: number | null, signed, deadline.
  • id: string | null, signed, external assignment id.
  • keys: Keys, partially signed. Author keys are MACed, student keys are not.
  • name: string, signed, assignment display name.
  • overdue: 'accept' | 'dock' | 'reject' | null, signed, local backendless overdue policy.
  • penalty: number | null, signed, percentage deduction used by the local dock policy.
  • report: Report, signed, scores and interventions.
  • issue: string, signed, deterministic blank-slate digest.
  • issuer: string, signed, author PGP signature over issue.
  • seal: string | null, unsigned, SHA-256 of concatenated ciphertexts.
  • mac: string, mutable rubric authenticity MAC.
  • certification: number | null, unsigned, final grade timestamp.
  • submission: number | null, unsigned, student submission timestamp written into the workbook.
  • submitted: string | null, unsigned, external submission receipt from a submitter authority.
  • distribution: number | null, unsigned, local distribution timestamp.
  • collected: string | null, unsigned, external collection receipt.

Certification Sequence

  1. correct() - execute cells, compute scores, sign report if all cells resolve
  2. certify() - write certification timestamp
  3. lock() - encrypt reference cells and roster, erase key
  4. freeze() - set cells to non-editable

A workbook cannot be collected without a non-null certification. Corrector can scan locked metadata, but scores require authenticated command results. Grade skipping, score export, and collection never trust a certification timestamp or stored report merely because it was parsed successfully.

Verification

  • Rubric.validate(rubric) - structural and authenticity checks: assignee must appear in roster, MAC must be valid if assignee or roster exists, issue and issuer must appear together and verify, author keys must be present, sealed assignments must have an author public key, assignee private key requires a corresponding public key.

Serialization Invariant

The required cxtformat discriminator identifies the persisted Correxit metadata format. It is authenticated by both the assignment issue digest and the rubric MAC. Missing and unknown formats are rejected before their contents are interpreted.

Format 2 is the only supported format. Format 1 was a draft with no usage in the wild and is superseded. Its authenticated terms did not bind issued reference sources, so it cannot supply the current security guarantees. Readers reject it rather than infer missing commitments from notebook contents. There is no automatic migration or legacy reader. The bundled example uses format 2. Package and workbook-format versions are independent.

All optional fields use Type | null, never Type?. This keeps JSON.stringify deterministic: null is serialized, undefined is omitted. Since MACs hash stringified JSON, field presence must stay stable.

Key Representation

The supplied or PBKDF2-derived key is a 256-bit value stored as a 64-character lowercase hex string. hmac decodes this to 32 raw bytes before importing it as HMAC key material. The same hex string is used as-is for the openpgp symmetric password.

Assignee Key Lifecycle

Assignee key fields start as null from propagation and remain null unless the Unlocker provides student credentials at submit time (the default provider's "Set passphrase" choice). Rubric.unseal() clears them back to null on revise. Fire-and-forget submissions therefore never offer a revise option.

Rubric.assign() preserves keys when reassigning, but it only operates on unlocked rubrics, where assignee key fields are always null. Stale student keys therefore cannot leak across assignments.

Known Limitations

  1. Client-side only. A technically sophisticated student could modify extension code in-browser to bypass locking or encryption. The cryptographic mechanisms detect tampering after the fact.

  2. No separation of duties. The author holds the key and can perform any operation. There is no independent audit role.

  3. Seal is unsigned. The seal hash is SHA-256, not HMAC. A student with file access and the cleartext public key could forge new ciphertexts and a matching hash. The seal detects corruption, not deliberate forgery. See Seal Integrity.

  4. Grading executes student code. Batch grading runs submitted notebooks in Jupyter kernels. Use an isolated grading Jupyter environment for adversarial submissions and trusted kernel specifications. Source authentication does not sandbox student code or guarantee honest execution in a shared kernel. Additional working cells, outputs, cell metadata, kernel selection, and supporting resource-file contents are outside the issued-source commitment.