Permit:   Signed Product Rights
1 Trust stores
read-trust-store
jsexpr->trust-store
trust-store?
trust-store-issuer
2 Verifying credentials
verify-credential
verified-credential?
verified-credential-type
verified-credential-id
verified-credential-issuer
verified-credential-issued-at
verified-credential-entitlement-id
verified-credential-revision
verified-credential-subject-type
verified-credential-subject-id
verified-credential-subject-display
verified-credential-channel
verified-credential-use-expires-at
verified-credential-updates-through
verified-credential-features-through
3 Evaluating policy
credential-usable?
credential-status
activation-valid?
trial-extension-valid?
edition-entitled?
feature-entitled?
update-entitled?
capability-value
limit-value
version-allowed?
select-latest-entitlement
4 Issuing credentials
generate-ed25519-keypair
read-ed25519-private-key
read-ed25519-public-key
ed25519-private-key->der
ed25519-public-key->der
public-key->base64
issued-credential?
issue-credential
write-issued-credential
5 Command line
6 Local trials and installation identity
generate-installation-id
make-local-trial
evaluate-local-trial
local-trial-evaluation-state
local-trial-evaluation-active?
local-trial-evaluation-reason
local-trial-evaluation-remaining-seconds
local-trial->jsexpr
jsexpr->local-trial
write-local-trial
read-local-trial
9.3

Permit: Signed Product Rights🔗ℹ

 (require permit) package: permit

Permit is a framework-neutral protocol and Racket reference implementation for answering one product question: what may this person, organization, or installation use right now? Long-lived entitlements describe commercial rights, optional short-lived activation leases bind a right to an installation, and local first-run trials are their own convenience state. Everything is verified offline against an Ed25519 trust store that ships inside the application; issuance and rotation are online concerns that never change the verification protocol.

Permit is deliberately not anti-tamper DRM, and a permanently offline credential cannot learn about a revocation that happened elsewhere — see the threat model for the honest boundaries. The normative protocol lives in Protocol v1; this manual documents the Racket surface.

1 Trust stores🔗ℹ

Applications ship public keys only. A trust store is JSON application configuration, delivered through the application’s authenticated release path:

{
  "protocol": "permit-trust-store",
  "version": 1,
  "issuer": "com.example",
  "keys": {
    "license-2027": {
      "algorithm": "Ed25519",
      "public_key": "<Base64 SubjectPublicKeyInfo DER>",
      "status": "active",
      "purposes": ["entitlement", "trial", "trial-extension"],
      "not_before": "2027-01-01T00:00:00Z",
      "not_after": null
    }
  }
}

Key statuses are next (pre-distributed, must have not_before), active (normal issuance), retired (no new issuance, must have an not_after issuance cutoff — old credentials stay authentic), and revoked (every credential under the key fails). Each key declares the credential types it may sign; an activation-signing key therefore cannot mint a perpetual entitlement. The time windows constrain the credential’s authenticated issued_at, not the wall clock at verification — retiring a key never kills a perpetual entitlement.

procedure

(read-trust-store source) → trust-store?

  source : (or/c path-string? input-port?)
Parses and validates a trust store document. Unknown key statuses, missing purposes, and wrong algorithms are rejected here.

procedure

(jsexpr->trust-store value) → trust-store?

  value : jsexpr?
The same validation for an already-parsed document.

procedure

(trust-store? v) → boolean?

  v : any/c

procedure

(trust-store-issuer store) → string?

  store : trust-store?
The only credential issuer this store will accept.

2 Verifying credentials🔗ℹ

A credential is a small JSON envelope whose payload is base64 bytes signed exactly with Ed25519. Verification authenticates those bytes first and only then parses authorization claims — the verifier never re-serializes JSON, which removes canonicalization ambiguity across languages.

procedure

(verify-credential source store) → verified-credential?

  source : (or/c path-string? input-port?)
  store : trust-store?
Verifies signature, trust key, issuer, key purpose, and key issuance window, then parses and schema-checks the payload. Any failure raises exn:fail — an invalid credential is an error, not a boolean.

procedure

(verified-credential? v) → boolean?

  v : any/c
Only values returned by verify-credential satisfy this predicate; the constructor is sealed so unverified claims cannot flow into policy checks.

The claims accessors (all (-> verified-credential? ...)):

procedure

(verified-credential-type c) → string?

  c : verified-credential?
"entitlement", "trial", "trial-extension", or "activation".

procedure

(verified-credential-id c) → string?

  c : verified-credential?

procedure

(verified-credential-issuer c) → string?

  c : verified-credential?

procedure

(verified-credential-issued-at c) → string?

  c : verified-credential?

procedure

(verified-credential-entitlement-id c) → (or/c string? #f)

  c : verified-credential?

procedure

(verified-credential-revision c) → exact-nonnegative-integer?

  c : verified-credential?

procedure

(verified-credential-subject-type c) → (or/c string? #f)

  c : verified-credential?

procedure

(verified-credential-subject-id c) → (or/c string? #f)

  c : verified-credential?

procedure

(verified-credential-subject-display c) → (or/c string? #f)

  c : verified-credential?
The customer-facing name (an email address, for example). Display only — never an authorization input.

procedure

(verified-credential-channel c) → (or/c string? #f)

  c : verified-credential?

procedure

(verified-credential-use-expires-at c) → (or/c string? #f)

  c : verified-credential?
Canonical UTC timestamp; #f means the right does not expire.

procedure

(verified-credential-updates-through c) → (or/c string? #f)

  c : verified-credential?

procedure

(verified-credential-features-through c) → (or/c string? #f)

  c : verified-credential?

3 Evaluating policy🔗ℹ

Policy evaluation is total and offline: every check takes the verified credential, an evaluation instant ((current-rfc3339) by default), and optional binding parameters.

procedure

(credential-usable? c 
  [#:at at 
  #:installation-id installation-id 
  #:activation activation 
  #:trial-extensions trial-extensions]) 
 → boolean?
  c : verified-credential?
  at : string? = (current-rfc3339)
  installation-id : (or/c string? #f) = #f
  activation : (or/c verified-credential? #f) = #f
  trial-extensions : (listof verified-credential?) = '()
True when the credential is currently within its use window, matches the installation binding if any, and carries a valid activation lease if the entitlement demands one.

procedure

(credential-status c 
  [#:at at 
  #:installation-id installation-id 
  #:activation activation 
  #:trial-extensions trial-extensions]) 
 → (hash/c symbol? any/c)
  c : verified-credential?
  at : string? = (current-rfc3339)
  installation-id : (or/c string? #f) = #f
  activation : (or/c verified-credential? #f) = #f
  trial-extensions : (listof verified-credential?) = '()
The full verdict: 'usable?, 'reasons (a list of symbols such as 'use-expired or 'not-active-yet), and 'effective_use_expires_at after trial extensions.

procedure

(activation-valid? entitlement    
  activation    
  installation-id    
  [#:at at]) → boolean?
  entitlement : verified-credential?
  activation : verified-credential?
  installation-id : string?
  at : string? = (current-rfc3339)
Checks that a lease is current and bound to the exact entitlement id, revision, credential id, issuer, and installation.

procedure

(trial-extension-valid? trial    
  extension    
  [#:at at]) → boolean?
  trial : verified-credential?
  extension : verified-credential?
  at : string? = (current-rfc3339)
Checks the signed relationship and activation time of a trial extension.

procedure

(edition-entitled? c 
  product 
  edition 
  [#:at at 
  #:product-family product-family 
  #:version version 
  #:installation-id installation-id 
  #:activation activation 
  #:trial-extensions trial-extensions]) 
 → boolean?
  c : verified-credential?
  product : string?
  edition : string?
  at : string? = (current-rfc3339)
  product-family : (or/c string? #f) = #f
  version : (or/c string? #f) = #f
  installation-id : (or/c string? #f) = #f
  activation : (or/c verified-credential? #f) = #f
  trial-extensions : (listof verified-credential?) = '()
True when a grant covers the product at the edition and the credential is usable. The same keyword shape applies to feature-entitled?, addon-entitled?, and update-entitled?.

procedure

(feature-entitled? c    
  product    
  feature    
  [#:published-at published-at    
  #:at at ...]) → boolean?
  c : verified-credential?
  product : string?
  feature : string?
  published-at : (or/c string? #f) = #f
  at : string? = (current-rfc3339)
Product-feature check with the published-through window: a feature published after features_through is not covered, so a perpetual customer keeps everything published inside the window they bought. Requires #:published-at — the application’s honest build-time or manifest timestamp for that feature.

procedure

(update-entitled? c    
  product    
  update-published-at    
  [#:at at ...]) → boolean?
  c : verified-credential?
  product : string?
  update-published-at : string?
  at : string? = (current-rfc3339)
Update-rights check against updates_through.

procedure

(capability-value c    
  product    
  capability    
  [#:default default    
  #:at at ...]) → any/c
  c : verified-credential?
  product : string?
  capability : symbol?
  default : any/c = #f
  at : string? = (current-rfc3339)

procedure

(limit-value c    
  limit    
  [#:default default    
  #:at at ...]) → any/c
  c : verified-credential?
  limit : symbol?
  default : any/c = #f
  at : string? = (current-rfc3339)
Scalar capabilities and seat limits (named seats, device activations, concurrent seats) from the credential’s constraints.

procedure

(version-allowed? c version) → boolean?

  c : verified-credential?
  version : string?
SemVer 2.0 check against the credential’s version bounds.

procedure

(select-latest-entitlement credentials) → verified-credential?

  credentials : (and/c (listof verified-credential?) pair?)
Deterministic replacement among credentials actually received: highest revision for the same entitlement_id. This is replacement, not offline revocation.

4 Issuing credentials🔗ℹ

 (require permit/issuer) package: permit

The issuer surface is an explicit opt-in; the client facade above never exports private-key operations.

procedure

(generate-ed25519-keypair) → 
pk-key? pk-key?

procedure

(read-ed25519-private-key source) → pk-key?

  source : path-string?

procedure

(read-ed25519-public-key source) → pk-key?

  source : path-string?

procedure

(ed25519-private-key->der key) → bytes?

  key : pk-key?

procedure

(ed25519-public-key->der key) → bytes?

  key : pk-key?

procedure

(public-key->base64 key) → string?

  key : pk-key?

procedure

(issued-credential? value) → boolean?

  value : any/c
Recognizes the opaque result of issue-credential.

procedure

(issue-credential payload    
  private-key    
  key-id    
  store) → issued-credential?
  payload : bytes?
  private-key : pk-key?
  key-id : string?
  store : trust-store?
The safe issuance path: schema-validates the payload, pins issuer, active key status, key purpose, and the key’s issuance window, and verifies the private key matches the trust-store public key. The raco permit issue command goes through this function; raco permit sign is the deliberate low-level escape hatch.

procedure

(write-issued-credential credential    
  destination) → void?
  credential : issued-credential?
  destination : (or/c path-string? output-port?)
Serializes an issued credential as its Protocol v1 JSON envelope. This is the supported programmatic path from safe issuance to storage or delivery.

5 Command line🔗ℹ

raco permit keygen --private k.der --public pub.der \

           --trust-store trust.json --key-id license-2027 \

           --issuer com.example

raco permit issue  --key k.der --key-id license-2027 \

           --trust-store trust.json --payload ent.json \

           --output customer.permit.json

raco permit verify --trust-store trust.json \

           --credential customer.permit.json

raco permit check  --trust-store trust.json \

           --credential customer.permit.json \

           --product example-app --edition pro

raco permit inspect --credential customer.permit.json

#

inspect is deliberately labelled unverified; never authorize a feature from inspected claims alone.

6 Local trials and installation identity🔗ℹ

procedure

(generate-installation-id) → string?

A random, privacy-preserving installation identifier — not a hardware fingerprint.

procedure

(make-local-trial [#:duration-seconds duration-seconds 
  #:at at 
  #:id id]) 
 → local-trial?
  duration-seconds : exact-positive-integer? = (* 14 24 60 60)
  at : string? = (current-rfc3339)
  id : string? = (generate-local-trial-id)

procedure

(evaluate-local-trial trial 
  [#:at at 
  #:extensions extensions 
  #:issuer issuer 
  #:rollback-tolerance-seconds tolerance]) 
 → local-trial-evaluation?
  trial : local-trial?
  at : string? = (current-rfc3339)
  extensions : (listof verified-credential?) = '()
  issuer : (or/c string? #f) = #f
  tolerance : exact-nonnegative-integer? = 300
Evaluates the trial against the stored clock high-water mark and any verified trial-extension credentials. The result carries the updated state to persist.

procedure

(local-trial-evaluation-state r) → local-trial?

  r : local-trial-evaluation?

procedure

(local-trial-evaluation-active? r) → boolean?

  r : local-trial-evaluation?

procedure

(local-trial-evaluation-reason r) → (or/c symbol? #f)

  r : local-trial-evaluation?

procedure

(local-trial-evaluation-remaining-seconds r)

 → (or/c exact-integer? #f)
  r : local-trial-evaluation?

procedure

(local-trial->jsexpr t) → jsexpr?

  t : local-trial?

procedure

(jsexpr->local-trial v) → local-trial?

  v : jsexpr?

procedure

(write-local-trial t destination) → void?

  t : local-trial?
  destination : path-string?

procedure

(read-local-trial source) → local-trial?

  source : path-string?
A local trial is signed nowhere — it is convenience state with a random id and a last-seen clock high-water mark. That is nuisance resistance, not tamper resistance; a user controlling the machine can reset it.