OPP Ruby
opp implements the Open Presence Protocol (OPP) 0.1 Presence Document
lifecycle and reusable OPP signing primitives for Ruby 3.2 and newer.
Installation
Add the gem to your bundle:
gem "open-presence", "~> 0.1.0"
Then run bundle install. To install it directly, run
gem install opp -v "~> 0.1.0".
Presence Documents
Generate an Ed25519 key pair and derive its OPP subject:
require "opp"
require "time"
pair = OPP::KeyPair.generate
subject = OPP::Subject.derive(pair.public_key)
pair.private_key is a secret 32-byte seed encoded as unpadded Base64url.
Do not log, publish, or commit it. Store it with the same protections as any
other signing credential. The public key and derived subject are safe to
publish.
Sign a Presence Document. Signing returns a new hash and does not mutate the input. The public key, subject, and signature are added by the library.
document = {
"type" => "open-presence",
"version" => "0.1",
"issued_at" => "2026-07-12T00:00:00Z",
"expires_at" => "2026-07-13T00:00:00Z",
"services" => [
{ "type" => "profile", "url" => "https://example.com/alice" }
]
}
signed = OPP::Presence.sign(document, private_key: pair.private_key)
Non-bang verification returns an OPP::VerificationResult. Pass at: when
you need a reproducible verification time; otherwise it defaults to the
current UTC time.
result = OPP::Presence.verify(
signed,
at: Time.iso8601("2026-07-12T12:00:00Z")
)
if result.valid?
puts "valid"
else
result.errors.each do |error|
warn [error.code, error.path, error.].compact.join(": ")
end
end
OPP::Presence.verify! returns true for a valid document and raises the
first structured OPP::Error for an invalid one:
begin
OPP::Presence.verify!(signed, at: Time.iso8601("2026-07-12T12:00:00Z"))
rescue OPP::Error => error
warn "#{error.code} at #{error.path}: #{error.}"
end
Errors expose a machine-readable code, an optional field path, and a
human-readable message. Specific failures use subclasses such as
OPP::ValidationError, OPP::InvalidSignatureError,
OPP::ExpiredDocumentError, and OPP::UnsupportedVersionError.
Unknown top-level fields, unknown service fields, and unknown service types are accepted. They remain part of the canonical signed payload, so changing or removing them invalidates the signature.
Generic signatures
OPP::Signature signs any JSON-object-shaped hash without applying Presence
validation. This is useful for other OPP objects:
registration = {
"type" => "open-presence-directory-registration",
"version" => "0.2",
"sequence" => 7,
"subject" => subject,
"extension" => { "enabled" => true }
}
signed_registration = OPP::Signature.sign(
registration,
private_key: pair.private_key
)
OPP::Signature.verify!(signed_registration, public_key: pair.public_key)
OPP::Signature.verify! returns true or raises
OPP::InvalidSignatureError. OPP::Signature.verify is also available when
a boolean result is preferable. Both signing APIs return deep copies rather
than mutating caller-owned hashes, and unknown and nested fields are
authenticated.
The generic API is responsible only for signature structure, canonicalization, key handling, and cryptographic verification. In particular, it does not validate Directory Registration schemas, sequence rules, persistence, networking, or discovery. Applications using generic signatures must validate their own object schema and protocol rules.
Vectors and interoperability
The offline conformance suite vendors the language-neutral OPP vectors under
spec/fixtures/opp. Their source repository, exact commit, retrieval date,
and verification time are recorded in spec/fixtures/opp/UPSTREAM.yml; the
current snapshot comes from jodydawkins/opp commit
73e4e761ad05ac5b6f4cd7ac3ad7c59842dadeba.
For reciprocal interoperability testing:
- Use the deterministic private-key seed from the pinned vector snapshot.
- Sign the same document with
OPP::Presence.signand serialize the returned hash as JSON. - Add the Ruby-produced JSON to the upstream/Rust interoperability fixtures and verify it there at the manifest's fixed verification time.
- Copy the corresponding Rust-produced signed fixture into the pinned Ruby
snapshot and run
bundle exec rspec spec/opp/conformance_spec.rboffline.
This gem deliberately has no CLI. Use the Ruby API directly or build application-specific commands around it.
License
Apache-2.0. See LICENSE.