Federation
Vultron has a design for a world in which each organization operates its own coordination service. This page gives the federation model for those services. It tells you which data the services interchange, which actor has authority for a case, and how two organizations build the trust that coordination needs.
The project has not built this model. The prototype operates more than one actor in a single deployment. Therefore this page gives a design direction, not a description of code that operates. Each open question in the text is unresolved, and each one identifies the issue or the epic that records it.
Federation between known peers, not open-web broadcast
The federation model is closer to email or to Matrix homeservers than to a public social network. Each organization operates a service. That service is a gateway between the internal tracker of the organization and the Vultron protocol. Neither organization needs data about the internal systems of the other organization. Both organizations speak the same protocol.
Federation occurs between parties that know each other. It is not open-web discovery, and it is not public broadcast. A human relationship or an inbound report puts two organizations together, and the protocol then carries the coordination. This condition controls almost all of the design decisions on this page. A closed group of known peers can use out-of-band trust, and it does not need the mechanisms of public federation.
AS2 supplies the vocabulary
Vultron uses ActivityStreams 2.0 (AS2) as the semantic vocabulary for each coordination message.
An AS2 activity is a sentence: it gives the actor, the object, and the time of one event.
Vultron declares its own object types and activity types as AS2 vocabulary extensions.
It uses a JavaScript Object Notation for Linked Data (JSON-LD) @context for these declarations.
Vultron uses AS2 as a message format. The prototype implements an actor and an inbox, and it implements no more of ActivityPub than that. This is the current condition of the work, and it is not a decision against the other parts.
Open question: how much of ActivityPub does a deployment implement?
Vultron uses AS2 as its message vocabulary. It also uses the actor, inbox, and outbox primitives from ActivityPub. The prototype stops there. It does not supply the other parts of an ActivityPub server:
- a NodeInfo document
- followers and following collections
- a shared inbox
- a client-to-server Application Programming Interface (API)
The prototype does supply a readable outbox collection for each actor. The project does not reject the missing parts. The prototype does not need them to show coordination between known peers. Thus the correct depth of ActivityPub conformance for a production deployment is open. Full conformance gives interoperability with existing fediverse tools. It also has a cost. A deployment makes endpoints that a closed group of trusted peers does not read, and it gives them protection.
There are two possible paths (#2068). A deployment can make the current AS2 endpoints fully conformant to ActivityPub. As an alternative, it can put Vultron above an existing ActivityPub server. Four criteria apply to this selection: conformance test coverage, maintainability, agreement with the behavior-tree core, and deployment footprint.
Actors, inboxes, and outboxes
Vultron uses the AS2 actor model as its coordination primitive. An actor is a peer with a Uniform Resource Identifier (URI). Each actor has an inbox, which receives activities, and an outbox, which sends them. A deployment also has instance-level actors for peering and for first contact from an unknown party.
The actor that holds CVDRole.CASE_MANAGER for a case coordinates that case.
Authority comes from the role and from nothing else (ADR-0088, CM-02-011).
The name of an actor, the shape of its Uniform Resource Locator (URL), and its host have no protocol meaning.
The prototype supplies a dedicated container actor for this function and gives it the name case-actor.
That name is a convenience (CM-02-013).
One such actor can hold the role for many cases at the same time.
The context field of an activity identifies the case, and the URI of the actor does not identify it.
An actor has a key pair, and the key material applies to the actor identity.
As an alternative, a deployment can use a case actor service to supply one CASE_MANAGER actor for each case. Each case then has its own actor, and encryption becomes per-case as a result. This is an intended use of Vultron. It needs a ready mechanism for actors to find each other, and actor discovery is an open question.
The parts of a case across instances
A case is a container for participants, reports, status, and history. A Participant is a case-scoped record that refers to a global Actor. It holds the roles and the authorization of that actor in this case. This difference is important, because one actor participates in many cases and holds different roles in each case.
---
title: Actors, Participants, and Cases
---
classDiagram
class Actor {
+id
+inbox
+outbox
}
class Participant {
+attributedTo
+caseRoles
+participantStatuses
+context
}
class Case {
+attributedTo
+caseParticipants
+caseActivity
}
Actor "1" --> "*" Participant : attributedTo
Case "1" --> "*" Participant : caseParticipants
The diagram shows that a Participant connects a long-lived Actor to one Case.
An Actor exists independently of a case.
A Participant exists only in one case.
The field names are the serialized AS2 names on as_CaseParticipant and as_VulnerabilityCase, and the diagram is not notional.
The attributedTo of a Participant holds the Actor, and the context of a Participant holds the Case.
Case ownership
The attributedTo field of a case holds an actor identifier, and it does not hold a host.
The CASE_MANAGER that makes the case is the value of that field (CP-09-001).
Ownership is unambiguous for a different reason: one Actor holds CVDRole.CASE_OWNER at a given time (CM-21-001).
A transfer of that role uses an Offer and Accept cycle.
The Case Activity Log gives the full history of ownership.
Any Participant Actor can accept a report and make a case from it. The Actor that makes the case supplies the CASE_MANAGER for it. That Actor also holds the CASE_OWNER role until a transfer occurs. The host of an actor gives no authority, and the role gives all of it (ADR-0088).
Open question: where is the enacting actor after an ownership transfer?
A case ownership transfer moves the CASE_OWNER role from one actor to another actor.
It uses an Offer and Accept handshake through the CASE_MANAGER (ADR-0053).
The handshake does not set the location of the actor that enacts CASE_MANAGER.
If the organization of the new owner administers the case, there are two results.
The enacting actor moves to that organization, or it stays in position and the previous organization continues to hold a part in the case.
There are four possible methods, and the project has not selected one.
Migration changes the URI of the actor and tells each participant the new URI. This method is clear. It also controls the messages that are in transit and the replicas that use the previous URI.
A proxy keeps the previous instance, which forwards each message to the new instance. This method is simple. But if a second ownership transfer occurs, the chain of proxies becomes longer.
A Hyper Text Transfer Protocol (HTTP) 301 redirect with an AS2 Move keeps the initial URI as the canonical URI.
A new instance answers at that URI.
The fediverse uses Move and movedTo for actor migration, and ActivityPub itself does not define the mechanism.
A new actor, with the previous actor in a frozen condition, is the minimum work to build. It is also the largest risk to verification. New key material breaks the continuity of signatures across the transfer.
The redirect method is the most practical method from the current data. This is a working preference, not a decision.
Concern #3362 records this question.
From report to case
First contact is the only interchange that occurs when no case exists.
---
title: Report intake and case creation across two instances
---
sequenceDiagram
participant A as Reporter (instance A)
participant B as Vendor (instance B)
participant CM as CASE_MANAGER
A->>B: Offer(Report)
Note over B: cold contact — no case exists
B->>CM: provision case and CASE_MANAGER
B->>A: Accept(Offer(Report))
CM->>A: Create(Case)
Note over A: case replica seeded
CM->>A: Add(Participant, target Case)
A->>CM: case traffic from here on
CM->>A: relayed activities from other Participants
The diagram shows the change from one cold-contact message to steady-state coordination. After the Reporter has a replica, each subsequent message is case-scoped and goes through the CASE_MANAGER.
Open question: what occurs when one Report becomes two Cases?
A Report is not a Case.
A Report is the object of the initial Offer.
A Case exists only after a recipient accepts that Offer.
Two Report formats are possible: plain text and JavaScript Object Notation (JSON) in the Common Security Advisory Framework (CSAF) format.
The project has not specified either format, and more formats can follow.
The CSAF 2.1 crosswalk compares the two object models, and Idea #5131 tracks how Vultron reads CSAF content in a Report.
The condition occurs when the same Report goes to more than one recipient. Each recipient can accept the Report and make its own Case from it. The protocol lets this condition occur, and this behavior is intentional. A recipient that wants more than one vendor in the coordination can make one Case and invite each vendor to it.
The frequent form of this condition is sequential, not concurrent.
A Reporter receives no answer to an Offer, and then it asks a Coordinator for aid.
The recipient of the second Offer accepts before the recipient of the first Offer accepts.
Therefore each Offer has an identifier that is unique to a minimum of the Report and the recipient.
The Reporter keeps a different record of each recipient and of the Case that each recipient makes.
The Reporter cannot join two Cases into one Case, because the Reporter owns neither Case. A merge becomes possible when the owner of one Case learns that the other Case exists. A proposed design is in ADR-0105. In that design, the owner of one Case offers to merge it into the other Case, and the owner of the other Case accepts or rejects the offer. The offered Case then becomes read-only. A redirect sends each request for the frozen Case to the Case that remains. The Case that remains invites each participant of the frozen Case. Two items are still open. The first item is the embargo obligations of a participant that declines the invitation. The second item is a design that keeps both Cases open and connects them.
Concern #3366 records this question.
Case traffic goes through the CASE_MANAGER
After a case exists, all case communication is a set of direct messages. These messages go between each Participant actor and the CASE_MANAGER. The protocol publishes no case data in public. Participants do not send messages directly to each other (PCR-08-001, PCR-08-002). One interchange is outside this rule. A Reporter sends a report directly to an Actor to start the process, and no case exists at that time.
One relay through one actor gives four properties, at the cost of one more network step. The CASE_MANAGER can apply authorization, because it sees each message and knows the roles of each Participant. It can also attest to the sequence, because it gives each entry its position. A Participant cannot send a message that seems to come from a different Participant, because only the CASE_MANAGER sends case traffic. The view of each Participant also comes from the canonical recorded log, and not from the messages that arrive.
Open question: which activities does each Participant receive?
A Participant holds case-scoped roles. The protocol settles the largest part of this question already. An Observer receives full case content, and no reduced delivery tier applies to a role (CM-25-003, ADR-0057). The gate on case content is admission to the case plus consent to the embargo, and it is not the role (MV-10-005).
The open part is smaller. It covers the single Activity, and it does not cover case content. A reduced tier for some Participants is a deferred mechanism, and the project has not specified it.
There are two possible locations for a routing policy of this type. The policy can be on the Participant, as metadata for a test against each activity. As an alternative, the policy can be with the CASE_MANAGER, as a set of role-based rules for fan-out. The second location agrees better with the other parts of the protocol. It keeps one actor responsible for the knowledge of each Participant.
For the two locations, the fan-out of the CASE_MANAGER continues to be a simple loop across the Participants. The per-Participant send makes the decision to relay an activity or not to relay it. A prototype can relay each activity to each Participant and stay correct. But the structure gives space for a complete set of rules.
Concern #3365 records this question.
Relay with Announce
The CASE_MANAGER sends the activity of one Participant to the other Participants.
It puts the initial activity in an AS2 Announce and signs the Announce.
{
"@context": "https://certcc.github.io/Vultron/ns/context.jsonld",
"type": "Announce",
"id": "https://vendorb.example/cases/1234/actor/outbox/relay/88",
"actor": "https://vendorb.example/cases/1234/actor",
"published": "2025-03-08T12:00:00Z",
"logIndex": 3,
"prevLogHash": "sha256-of-entry-2",
"to": "https://vendora.example/users/alice",
"object": {
"type": "Create",
"actor": "https://vendora.example/users/alice",
"object": {
"type": "Note",
"content": "Reproduced on version 4.2"
},
"signature": "...the reporter's original signature..."
},
"signature": "...the CASE_MANAGER's signature..."
}
The two signatures have two different functions. The inner signature shows that the activity came from the Participant that claims it. The outer signature shows that the CASE_MANAGER received the activity and relayed it, at a known position in the case ledger. A recipient verifies each signature independently. A recipient can thus trust a relayed message without a direct connection to the Participant that sent it.
Each outbound message declares the Vultron JSON-LD context, and the ActivityStreams namespace alone is not sufficient (VM-10-001).
The example shows the logIndex and prevLogHash fields on the Announce for clarity.
The implementation carries them on the as_CaseLedgerEntry object instead.
The logIndex and prevLogHash fields let a recipient put the relayed activity in its replica immediately.
The recipient does not wait for a synchronization cycle.
Open question: what does a deployment sign and encrypt?
The prototype does not sign or encrypt its messages. Activities move as plaintext AS2 JSON on HTTP. The adapter for signed remote delivery is a stub (OX-10). The prototype is a demonstration of coordination logic, not a service for deployment. Concern #509 records this gap.
Encryption has a design.
specs/encryption.yaml (ENC-01 through ENC-03) gives each actor a key pair.
It publishes the public key in the actor profile.
It also puts decryption in the inbox handler, before semantic extraction.
Each handler thus continues to receive typed and validated activities.
Each requirement in that file has scope: production.
The requirements apply to a deployment, and the prototype is not a deployment.
Signing has no specification. The intended shape puts two signatures on a relayed activity. The inner signature comes from the Participant that originated the activity. The outer signature comes from the CASE_MANAGER that relayed it. A recipient can thus verify the source and the relay position independently. HTTP Message Signatures is the candidate transport mechanism, and Request for Comments (RFC) 9421 defines it (#892, #1163).
Three points are open. First, the project has not selected the requirements that apply to a deployment that claims Vultron conformance. Second, per-recipient encryption (ENC-02-002) can be sufficient for a case that has many Participants, or a multi-recipient envelope format can be necessary. Third, key material applies to an actor identity, not to a case. The quantity of cases that one CASE_MANAGER actor holds thus sets the separation between the keys of those cases. Epic #1156 records this work, and the actor identity model is open (#2841).
The case ledger and the delivery log
The CASE_MANAGER keeps two collections and shows them as AS2 named streams. The two collections have different functions.
| Collection | Content | Synchronized |
|---|---|---|
Case ledger (/outbox) |
Hash-chained record of the case events, indexed by logIndex |
Yes — this is the replication target |
Delivery log (/streams/delivery) |
The Announce relays, with the recipient and the time of each one |
No |
The case ledger is append-only.
Each entry holds the hash of the entry before it, which keeps the ledger tamper-evident.
The case ledger holds the events of the case: Create, Update, Offer, Accept, Add, Remove, and the others.
Only ledger entries use logIndex positions.
The delivery log is a tool for operators.
It gives data for diagnosis, for retry control, and for delivery verification.
An operator can remove or archive the delivery log, and the integrity of the case stays correct.
The delivery log is also much larger than the case ledger.
One Note to twenty Participants makes one ledger entry and twenty delivery log entries.
Open question: does a case ledger need distributed consensus?
Each case has one writer. The actor that enacts CASE_MANAGER is the only author of the canonical hash-chained case ledger. Each Participant Case Replica is a projection of that ledger (PCR-03-001, PCR-03-002). This is an intentional simplification. This single-writer regime needs no consensus protocol. The hash chain also keeps the history tamper-evident, and each Participant can verify it independently.
It is not clear if one writer is correct for each case. A case that has two parties, a finder and a vendor, has too few Participants for useful consensus. A case can also have many vendors, coordinators, and deployers. Such a case can possibly divide the coordination authority across the Participants, and not hold it in one actor. The append-only hash-chained ledger has properties now that a consensus artifact needs.
The working position is that a simple case keeps one CASE_MANAGER. A distributed model adds to that position, and it does not replace it. Epic #1158 records this research. The project defers the work until the current protocol model becomes stable.
Open question: do Participants acknowledge delivery?
The delivery log of the CASE_MANAGER records each message that it sent, and the time. It cannot record which messages the Participants received. A Participant can send a receipt activity when a message arrives. The log then shows the difference between confirmed delivery and best-effort delivery. Retry logic then has better data than a time-out.
Receipts have a cost in message volume. Thus receipts are optional, not mandatory. A prototype can record its delivery attempts and supply no receipts. But the architecture lets a later version add receipts without a change to the core protocol.
The activity type for a receipt is unsettled.
Read is too strong, because it shows that the Participant processed the activity.
A Receive or Ack activity is the weaker statement.
It also gives the delivery log the data that the log needs.
Concern #3363 records this question.
Convergence of the replicas
Each Participant keeps a Participant Case Replica, and the CASE_MANAGER pushes each change to it.
The CASE_MANAGER sends each Case Ledger Entry at the time of the event, which is ledger fanout.
Each entry holds its logIndex and prevLogHash, and the recipient uses these fields to put the entry in sequence.
Each entry also connects to the entry before it, and the CASE_MANAGER signs it.
A Participant can thus verify the integrity and the authenticity of the stream on arrival, and it does not trust the transport.
Push is the only specified path.
A Participant records the logIndex of each entry that it received.
A discontinuity in those values shows that an entry is absent.
Ledger reconciliation then repairs the gap.
It is a loop on the side of the CASE_MANAGER, with retry and backoff (SYNC-00-007).
The loop continues until the tail hash of the replica agrees with the tail hash of the CASE_MANAGER (SYNC-00-008).
The recipient buffers each entry that arrives out of sequence, and replay is the backstop for an entry that is genuinely lost (ADR-0037).
A read path for a Participant is a possible addition, and the protocol does not specify one.
A GET of the /outbox of the CASE_MANAGER, as a paginated AS2 OrderedCollection, is the expected shape of such a path.
Open question: how strong are the necessary ordering guarantees?
Ledger fanout does not occur immediately. The CASE_MANAGER relays a Case Ledger Entry to Participants on more than one instance. For a short time, those Participants can see the activities in different sequences.
The current position is that eventual consistency is sufficient.
The project knows of no part of the protocol that uses the sequence of arrival.
The logIndex value of each entry holds the correct sequence.
A Participant that receives entries 1, 2, 3, and 5 finds that entry 4 is missing.
It buffers entry 5, and ledger reconciliation delivers entry 4 (SYNC-00-007, ADR-0037).
The Participant does not use a history that has missing entries.
Gap detection replaces delivery order.
One point is not tested. The project does not know if a coordination decision needs a stronger guarantee than eventual consistency.
Task #5102 audits the protocol decisions for a dependency on the sequence of arrival.
Instance identity and trust
Instances send messages on Hyper Text Transfer Protocol Secure (HTTPS) with authenticated transport. Mutual Transport Layer Security (mTLS) between instances is the preferred mechanism. It authenticates at the transport layer, and it adds no work to each message. Each actor also signs its activities with its key pair. The transport thus has no effect on non-repudiation.
The Domain Name System (DNS) holds the trust anchor for the identity of an instance. This model follows DomainKeys Identified Mail (DKIM) and Mail Exchanger (MX) records for email.
- The operator publishes a DNS TXT record at its domain with the fingerprint of the instance public key.
- Two operators interchange domain names out of band, through human channels.
- The instance that connects reads the DNS TXT record of the peer and records the fingerprint.
- It gets
/.well-known/vultron-meta.json, which holds the full public key, the inbox URL, and the vocabulary extensions. - It compares the public key against the fingerprint from DNS.
- It sends a signed
Followactivity to the instance inbox of the peer. - The peer does the same checks and answers with
AcceptorReject. - Both instances keep a local record of the peering.
The out-of-band step is the most important step. It makes trust between the organizations, which is different from cryptographic verification. Cryptographic verification cannot give organizational trust.
An invite token is a possible addition. One operator makes a signed token and sends it out of band. The other operator sends the token with its peering request, which shows that an invitation exists. A directory is not necessary for this check.
Open question: how do actors find each other?
Coordination starts when one party can contact another. An actor needs the inbox address of a peer. It also needs the public key of that peer, its embargo policy, and its disclosure policy. The protocol does not say how an actor finds this data.
The problem is older than the protocol. An embargo divides the world into the Participants who belong in a case and everyone else, and knowing who a party is does not say why it is relevant to a case. Authentication is not authorization. In a small case the answer is a web search for the affected Vendor's disclosure contact. In a case with many Vendors that approach does not scale, for reasons the 2022 Vultron technical reports set out:
- Contact information for every relevant party is slow to collect by hand.
- Vendors accept reports through different channels, such as email, a web form, or an account on their own bug tracker, which prevents automated notification.
- Which other Vendors' products contain an affected component is often unknown, so a case cannot follow the software supply chain.
- A vulnerability in a protocol or a specification affects every implementation, and the implementers are hard to enumerate.
- Some Vendors treat the components of their products as confidential, which hides that a product is affected.
Community directories show the value of a shared answer.
The FIRST member list and Disclose.io each publish disclosure contacts, and Disclose.io accepts community contributions.
A Vendor can also publish a security.txt file (RFC 9116) so that a Reporter finds its contact without a directory.
Standard contact records with an interface to read them, standard contact methods, publication and aggregation of software bills of materials, and a way for Vendors to register interest in a technology would each remove one of the obstacles above.
The last of these has a cost: an adversary, or a competitor, can register interest in a technology to receive reports about products it does not make.
Discovery has two levels, and the two levels are open. At the actor level, an actor cannot resolve a name to an actor profile. A WebFinger-compatible mechanism is the expected shape, because it fits existing fediverse tools (#1189). The profile record is already an ActivityStreams actor with a small number of Vultron extensions. At the instance level, a voluntary public registry lets operators find each other at all. A registry is useful, but it is secondary to the peering protocol.
A registry has questions that the peering protocol does not have. The operator of a registry is unsettled, and a neutral foundation, a git repository, and a DNS zone are all possible answers. The content is also unsettled. A registry can hold only domain names, with DNS TXT records as the trust anchor, or it can hold more data. The method to retire stale or offline instances is a third open point.
The reference implementation reserves the seam. Its behavior trees have an actor-discovery call-out (#2469), and the backend that answers it is not built. Demo deployments do not have this problem. Each container receives the URL of each other container at start-up (CP-08). This is an alternative to a directory service, and the specification records it as one.
Delivery
Each instance writes its outbound activities to a durable queue before it sends them. Workers then send each activity to the inbox of the peer on HTTPS, with retry and backoff. A prototype can use synchronous delivery below this interface. But the architecture gives space for asynchronous delivery with retries.
Two refinements are important at large scale. An instance sends one request for more than one Participant on the same peer instance, and not one request for each Participant. A shared inbox makes this single request possible. A shared inbox receives activities for each local actor, and it sends them to the correct actor in the instance. The prototype does not supply one, and OX-11 gives the requirements for it.
Connectors
The coordination service is independent of the issue tracker. It speaks the Vultron protocol, and it has no data about any one tracker. Each deployment adds a connector for its own internal tracker. The connector translates between the events of the internal tracker and Vultron activities, in both directions.
The service finds its connectors as plugins at start-up. Support for a new tracker thus needs no change to the service.
Extension of the vocabulary
The object types and activity types of Vultron are extensions to AS2, and not replacements for it.
Vultron declares each extension type in a JSON-LD @context.
A peer uses that declaration to recognize a Vultron activity.
Open question: who controls the Vultron vocabulary?
Vultron adds its own object and activity types to AS2.
It declares them as JSON-LD @context extensions.
An extension vocabulary that more than one organization uses needs a version and governance model.
Vultron does not have one.
Three parts are unresolved.
The first part is the procedure to propose and to ratify a new activity type or object type, and the group that does it.
The second part is the method for two instances to agree at peering time on the vocabulary versions that both instances support.
A peer that does not recognize a type cannot act on that type.
The third part is the URI of the @context document and the operator that hosts it.
A @context document that a peer can resolve makes the vocabulary interoperable instead of local.
Concern #3368 records this question.
Further reading
- Glossary — definitions for CASE_MANAGER, case ledger, Case Ledger Entry, Participant Case Replica, and the other terms on this page
- Actor Knowledge Model — the limits of the knowledge of an actor, and how a recipient obtains the objects a message names
- Case Ledger Synchronization — replication in the current single-deployment implementation
- Ownership Transfer — the transfer sequence in the current implementation
- Open questions — each open question on this page, collected