Blue Badge 0.2.0

Posted 2024-11-26 14:00 ‐ 8 min read

I released Blue Badge in August 20241 as an experiment in how to incorporate attestation into ATProtocol. This small addition to the existing tools and technologies received a lot of good feedback and helped identify some questions that we're asking in the community:

  • How can one or more parties attest to a claim?
  • How do the existing ATProtocol technologies support this behavior?
  • What can be done to reduce the friction of attestation?

After some time and experimentation, the next iteration is here, and I'm excited to share it with you.

Context and Background

Every identity in the ATMosphere and Bluesky ecosystem contains data defined in the DID-CORE2 specification. The one relevant to this document is verification methods.

Within the ATMosphere, verification methods are used to cryptographically sign records in your PDS3. Only you (and to and through your PDS) have the keys to sign the content, which is definitive proof that the content belongs to you. That proof is transmitted when your posts are broadcast through the event stream4.

Although standard practice during sign-up is to create a single verification method for your PDS, you can have more than one.

0.2.0

The next iteration of the Blue Badge spec incorporates signatures referencing DID verification methods and includes a standard type for representing signature content within records.

Verification Method

A verification method for a typical DID-PLC document includes content that looks like this:

{
    "verificationMethod": [
        {
            "id": "did:plc:vdji24mx5mz2aiuv63ddxoy6#atproto",
            "type": "Multikey",
            "controller": "did:plc:vdji24mx5mz2aiuv63ddxoy6",
            "publicKeyMultibase": "zQ3shiadvzwi8x9HzGJXYpT9VZfDcdAChSYh8M67bkdJTGTXo"
        }
    ]
}

The "verificationMethod" list contains a public key that can be used to verify signatures. This spec introduces an additional key with an identifier fragment like "#bluebadge" or "#smokesignal":

{
    "verificationMethod": [
        {
            "id": "did:plc:vdji24mx5mz2aiuv63ddxoy6#smokesignal",
            "type": "Multikey",
            "controller": "did:plc:vdji24mx5mz2aiuv63ddxoy6",
            "publicKeyMultibase": "zQ3shiadvzwi8x9HzGJXYpT9VZfDcdAChSYh8M67bkdJTGTXo"
        }
    ]
}

This additional verification method can be created and appended to DID documents through tools like Goat and Tandem. Modifying your did-method-web or did-method-plc document is privileged, so it is intentionally difficult. That may change with time.

Signed Records

A signature of record data can be created and appended to records with a signing key from your DID document verification methods. Any record can be signed, although not all tools will use it.

When a record is signed, the signature is presented in the "sigs" attribute. It looks like this:

{
    "sigs": [
        {
            "signature": "MzQ2Y2U...CAgLQo=",
            "issuer": "did:plc:vdji24mx5mz2aiuv63ddxoy6#smokesignal"
        }
    ]
}

The "sigs" collection includes zero or more signature blocks that contain the signature issuer, the signature created from the record, and any additional values used to compose the signature. The issuer DID can be resolved to get the verification key.

Creating Signatures

A signature is generated by taking a DAG-CBOR representation of the record and applying the signing function described in the verification method.

The signed content does not include the "sigs" attribute and also includes a "$sig" object containing additional information used to secure the signature. The "issuer," "did," and "collection" attributes are required to provide scope to the signature and prevent duplication of the signature in other repositories or collections. The "issuer" element is specifically used to resolve the verification method to validate the signature.

Additional optional signing elements supported include:

  • notBefore - A timestamp indicating the optional start time that a signature is valid for.
  • notAfter - A timestamp indicating the optional end time that a signature is valid for.
  • revocation - A reference to another record that will exist if the signature is revoked and contains revocation affirmation.
  • signedAt - A timestamp for when the signature was created.

Signatures are appended with the last signature of an issuer being validated. This behavior allows issuers to append and replace signatures with a fixed time window for each to be valid. Applications decide whether or not to keep signature histories on records.

Signature Validation

Any application or system using the record to make decisions must validate signatures using this process:

  1. Fail validation if a signature is outside of "notBefore" or "notAfter"
  2. Fail validation if a signature block has a "did" attribute that does not match the AT-URI
  3. Fail validation if a signature block has a "collection" attribute that does not match the AT-URI
  4. Fail validation if the issuer cannot be resolved
  5. Fail validation if the issuer key cannot be not resolved
  6. Fail validation if the computed signature does not match the given signature.
  7. Fail validation if a revocation entry exists and the value of that entry matches the revocation type where the "revoked" value is true.

Example: ATProto Camp

ATProto Camp awards badges to users for their activity in the ATMosphere.

@atproto.camp's "Adventure Awaits!" badge is defined as:

{
  "$type": "blue.badge.definition",
  "image": "https://atproto.camp/static/6XXzn32gbU.png",
  "name": "Adventure Awaits!",
  "text": "You're in for an adventure! Log in for the first time to https://atproto.camp/."
}

When the badge is awarded to a user, the award record includes a reference to the badge, as well as a signature:

{
    "$type": "blue.badge.award",
    "badge": {
        "cid": "bafyreifbq7wub6wfuntruagvaaivqsinxyc4mpbfagjka35v4wx7aeu3fe",
        "description": "You're in for an adventure! Log in for the first time to https://atproto.camp/.",
        "name": "Adventure Awaits!",
        "uri": "at://did:plc:puy52u7opoy3gvrv7h7qdy76/blue.badge.definition/6XXzn32gbU"
    },
    "did": "did:plc:cbkjy5n7bk3ax2wplmtjofq2",
    "issued": "2024-08-26T22:14:02.000Z",
    "sigs": [
        {
            "issuer": "did:plc:puy52u7opoy3gvrv7h7qdy76",
            "signature": "2-EaPZELcvu5SL8lS863fta8moqLZcpKlrzFpn7RbUr_B37HZphJa642dJfGNM2BMZGl-YGQync-2pyhoPC4Wg==",
            "issuedAt": "2024-08-26T22:14:02.000Z",
        }
    ]
}

These one-time badges don't need to be revoked, nor do they expire.

Example: Smoke Signal

Smoke Signal allows users to RSVP to events. Although most public events are free and open to the public, some events may have limited seats. In those cases, an event organizer may want to approve reservations.

The event at://metro-parks/events/1 is created as "Goat Yoga" with 10 spots open. Smoke Signal has a securely stored copy of the verification method signing key for @metro-parks.

Later, the RSVP at://yoga-dude/rsvps/200 is created. Smoke Signal is configured to automatically accept the first 10 people, thus signing the RSVP on @metro-park's behalf.

The original RSVP record looks like this:

{
    "$type": "events.smokesignal.calendar.rsvp",
    "status": "events.smokesignal.calendar.rsvp#going",
    "subject": {
        "uri": "at://metro-parks/events/1",
        "cid": "bafyrei...s5ebw7q"
    }
}

Before Smoke Signal creates the RSVP record on behalf of @yoga-dude, it injects several additional attributes into the record to be used in the signing process:

{
    "$type": "events.smokesignal.calendar.rsvp",
    "status": "events.smokesignal.calendar.rsvp#going",
    "subject": {
        "uri": "at://metro-parks/events/1",
        "cid": "bafyrei...s5ebw7q"
    },
    "$sig": {
        "issuer": "did:plc:metro_parks_did",
        "did": "at://did:plc:yoga_dude_did",
        "collection": "events.smokesignal.calendar.rsvp",
        "revocation": "at://did:plc:metro_parks_did/revocations/3y3ra3rz61"
    }
}

The signed record that is written to at://yoga-dude/rsvps/200 looks like:

{
    "$type": "events.smokesignal.calendar.rsvp",
    "status": "events.smokesignal.calendar.rsvp#going",
    "subject": {
        "uri": "at://metro-parks/events/1",
        "cid": "bafyrei...s5ebw7q"
    },
    "sigs": [
        {
            "signature": "85df455...a769ebe",
            "issuer": "did:plc:metro_parks_did#smokesignal",
            "revocation": "at://did:plc:metro_parks_did/revocations/3y3ra3rz61"
        }
    ]
}

SmokeSignal can revoke the signature on behalf of @metro-parks should they decide to invalidate the RSVP acceptance.

Rough Edges

I'm happy with the progress, but I acknowledge some rough edges.

First, introducing a new verification method is intentionally difficult. Currently, it requires either directly modifying your DID document or going through the PLC signing process with your PDS. The process is intentionally slow and deliberate because it isn't something everyone should do.

This process could be improved in a few ways. The first would be to make record signing a default behavior in the protocol. Generating a secondary signing key or allowing applications to request one on your behalf could be explored.

Second, the process of signing a record is bespoke and could be made easier. A third-party signing "escrow" service could be useful to provide users with a UI that makes it clear what record is going to be signed and by whom. Even better would be if this component were a part of the PDS user interface to make incremental signature changes a possibility.

Third, lexicon records don't support composition with other types. Supporting the behavior of traits, interfaces, or mix-ins would make it easier for ATMosphere developers to incorporate signature creation and validation without having to modify their current types or methods directly.

Lexicon

{
    "lexicon": 1,
    "id": "blue.badge.signature",
    "defs": {
        "main": {
            "type": "record",
            "description": "A signed record",
            "key": "tid",
            "record": {
                "type": "object",
                "properties": {
                    "sigs": {
                        "type": "ref",
                        "ref": "#collection"
                    }
                },
                "required": [
                    "sigs"
                ]
            }
        },
        "collection": {
            "type": "array",
            "items": {
                "type": "ref",
                "ref": "#signature"
            }
        },
        "signature": {
            "type": "object",
            "properties": {
                "signature": {
                    "type": "string"
                },
                "issuer": {
                    "type": "string",
                    "format": "did"
                },
                "did": {
                    "type": "string",
                    "format": "did"
                },
                "collection": {
                    "type": "string",
                    "format": "nsid"
                },
                "notBefore": {
                    "type": "string",
                    "format": "datetime"
                },
                "notAfter": {
                    "type": "string",
                    "format": "datetime"
                },
                "revocation": {
                    "type": "string",
                    "format": "uri"
                }
            },
            "required": [
                "signature",
                "issuer"
            ]
        },
        "revocation": {
            "type": "record",
            "description": "A record of revocation of a signature",
            "key": "tid",
            "record": {
                "type": "object",
                "properties": {
                    "revoked": {
                        "type": "boolean"
                    },
                    "message": {
                        "type": "string"
                    }
                },
                "required": [
                    "revoked"
                ]
            }
        }
    }
}

Discussion and Debate

I think record signing and validation is a reasonable approach to solve some issues that come with decentralized systems and data. It builds off of the work and concepts already present in ATProtocol, and is an optional tool that developers can incorporate in their applications. This is still early, and I'm confident it will evolve with discussion and implementations, but it's a good start. If you have thoughts, questions, or comments, message me: @ngerakines.me