Representing SSH Keys

What about using JSON-LD index maps? 8.6 Index Maps / 6.16 Data Indexing
If we go with the JSON-LD 1.1 Draft(?), one could also use ID Maps.

An ID map the would look like below.

{
  "@context": { /* ... */ }
  "id": "https://example.com/foo"
  "type": "Person"
  // ...
  "keys": {
    "https://example.com/foo#main": {
      "kty": "RSA",
      "use": "sign",
      "n": "...",
      "e": "...."
    },
    "https://example.com/foo#ssh1": { // Edit: This means SSH key #1, not SSHv1 key.
      "kty": "RSA",
      "use": "auth",
      "n": "...",
      "e": "...."
    },
    "https://example.com/foo#ssh2": { // Edit: This means SSH key #2, not SSHv2 key.
      "kty": "RSA",
      "use": "auth",
      "n": "...",
      "e": "...."
    },
    "https://example.com/foo#gpg": {
      "kty": "RSA",
      "use": "sign",
      "n": "...",
      "e": "...."
    }
  }
  // ...
}

If we cannot use 1.1, then we could try slightly change the current publicKey approach.
Like, the keyId as show in the Mastodon example is not a valid JSON-LD id (it is not correctly dereferenceable). It seems like the key id is the one in the URL hash, which then is “extracted” from the publicKey attribute.
One could instead search for the key using it’s key id (i.e. just the part after in the has) in the keys map.
But it is possible that I misunderstood how the keyId for the HTTP Signatures in the Fediverse work.

I think we should keep it simple and just keep it as an array. What if somebody has multiple SSH2 keys?

My intention with ssh1 and ssh2 was to show exactly that, but I forgot about SSHv1 vs SSHv2 :smiley:
Like, it’s a key id, not the type, so ssh1 and ssh2 mean “My SSH key #1” and “My SSH key #2”.

This is why it should just be an array. Like I said, keep things simple.

To me it’s simplicity in representation (array) vs simplicity in processing (map).

Like, I as an implementor would really prefer to not have an array I have to search trough find the right key for an ID.
(key = keys[id] is much simpler than key = null; for(curr_key in keys){ if (curr_key.id == id){ key = key; break; } }. And should you really want to loop, nobody is holding you back doing for(curr_key in keys.values()) { /* ... */ }).
And I think searching for the key for an given ID happens more often than taking all keys into account.

How is it simpler to process a map of keys that can be literally anything? Or put differently: how do you know what id is ahead of processing the map? Most importantly: what stops you from deserializing an array into a map and then processing it that way?

This goes back to “extract a specific key” vs “process all keys”.

Your proposal is simpler if you don’t care about a specific key and just want all of them.
My proposal is simpler if you don’t care about all keys but just about a specific one (you know the id of).
Can we agree on that?

My further argumentation is that the “extract a specific key” use-case will appear more often than the “process all keys”, so in my argumentation the simplicity of extracting a key which “literally anything” ID you know is more “important” than simplicity of “processing all keys”.

Either I search for a specific one and know it.
Or I don’t care about a specific key, ignore them and just process the values.
And extracting values from a map, ignoring the keys is, AFAIKT built-in to all map implementations out there.

Nothing, but it’s an unnecessary burden for a frequent use-case (find key by id) in favour of an less frequent use-case (just process all keys).
And, as extracting values is simple (as above), the burden for the less frequent use-case is small, if not non-existent.

We can agree that your proposal optimizes for looking up an individual key by a known identifier. My point is that in reality, this is not what is ever going to happen.

Consider this: you have a signature or encrypted envelope – how do you know what key object ID you are looking for ahead of time? In most PKI applications, the public key is derived and then matched against a keychain.

For SSH, you would want to match against all the keys: authorized_keys is a keychain. For verifying signatures, again, you want the keychain.

The only case I can think of where you want to find a specific key with pre-determined knowledge of what key you’re looking for is signing or encrypting things, e.g. private keys. And of course nobody would be publishing their private keys, as that would make the whole exercise pointless.

Just to be extremely clear on this: literally the only handling of SSH keys a peer would be doing is dumping them into a keychain, removing them from a keychain or displaying them as part of a report. There isn’t anything else to be done with SSH keys.

Ok, well, you won :smiley:

I think I was too focused on resolving the key given in the Signature header for HTTP Signatures; and as http signatures are seem to work with a specific key, there you would have an frequent use-case with knowlegde which key you want.

HTTP signatures keyId is useful, but it’s not really a case to optimize for in the wire representation. The proper way to handle that is to cache the keys in a hot cache. After all, if you’re fetching a user for every request, you’re already losing.

The key rules with engineering this stuff is to keep things simple, but also keep things in a way that can be consumed by other standards too. Like with bearcaps being usable outside the AP space, we want to extend the security vocabulary that already exists so that other projects can help us maintain these concerns jointly.

Sooo this then? :smiley:

{
  "@context": { /* ... */ }
  "id": "https://example.com/foo"
  "type": "Person"
  // ...
  "keys": [
    {
      "id": "https://example.com/foo/key/main",
      "kty": "RSA",
      "use": "sign",
      "n": "...",
      "e": "...."
    },
    {
      "id": "https://example.com/foo/key/ssh1", // N.B. ssh key #1, not SSHv1
      "kty": "RSA",
      "use": "auth",
      "n": "...",
      "e": "...."
    },
    {
      "id": "https://example.com/foo/key/ssh2", // N.B. ssh key #2, not SSHv2
      "kty": "RSA",
      "use": "auth",
      "n": "...",
      "e": "...."
    },
    {
      "id": "https://example.com/foo/key/gpg",
      "kty": "RSA",
      "use": "sign",
      "n": "...",
      "e": "...."
    }
  }
  // ...
}

Yep looks good to me. That is precisely what I was thinking.

1 Like

Ah, the context keys comes from is still open.

I think we had two options so far

  1. W3 Security Vocabulary
  2. LitePub

The Security vocabulary is used to enable Internet-based applications to encrypt, decrypt, and digitally sign information expressed as Linked Data. It also provides vocabulary terms for the creation and management of a decentralized Public Key Infrastructure via the Web.
cf.

W3 Security has their own key format (PEM), as already discussed.
As we decided to use JWK, I am not sure what advantages the W3 Security vocabulary would bring us, or what advantages it would bring to the vocabulary to use JWK instead of PEM.

There already have been discussion in W3 security about integrating JWK and W3 Security vocabulary: https://web-payments.org/minutes/2012-07-24/#topic-1 but does not seem like there has been progress.

Looks like LitePub is the easier way to go? ^^

For go-fed processing ActivityStreams, anything that is a JSON object {} “grab-bag” without well-defined @type and domain of properties is going to be a non-starter. This is why endpoints sucks, this is why the current representation of a Key type in Mastodon (which omits the "@type": "Key") sucks. None of them have a "@type" and are thus poorly defined values from the perspective of JSON-LD.

The earlier proposal to use the property like so:
"keys": { "myKeyName1": {}, "someKeyName2": {}}
falls into this bucket of pain and suffering of a grab-bag of “properties”. Not good.

Much better is ensuring ActivityStreams ontologies and RDF usage are all strongly typed. In this case, where the "keys" property is non-functional and has a range of JWK or whatever format is agreed upon. So I support the simple array version that @kaniini advocates:
"keys": [ {"@id": ... }, {"@id": ... }]

{
  "@context": { /* ... */ }
  "id": "https://example.com/foo"
  "type": "Person"
  // ...
  "keys": [
    {
      "id": "https://example.com/foo/key/main",
      "type": "jwt:Key",
      "kty": "RSA",
      "use": "sign",
      "n": "...",
      "e": "...."
    },
    {
      "id": "https://example.com/foo/key/ssh1", // N.B. ssh key #1, not SSHv1
      "type": "jwt:Key",
      "kty": "RSA",
      "use": "auth",
      "n": "...",
      "e": "...."
    },
    {
      "id": "https://example.com/foo/key/ssh2", // N.B. ssh key #2, not SSHv2
      "type": "jwt:Key",
      "kty": "RSA",
      "use": "auth",
      "n": "...",
      "e": "...."
    },
    {
      "id": "https://example.com/foo/key/gpg",
      "type": "jwt:Key",
      "kty": "RSA",
      "use": "sign",
      "n": "...",
      "e": "...."
    }
  }
  // ...
}

? :slight_smile:

We can create a security vocabulary in LitePub if desired. However, I would like to discuss that on tomorrow’s informal SocialCG call before doing it, as action items for LitePub can still be tabled and discussed, we just cannot do anything W3C-facing.

Thank you both for all the thoughts :slight_smile:
@kaniini, any news on the location of ‘keys’?

I also wonder whether plural ‘keys’ is a good name because it’s essentially an RDF property, which relates between an actor and a single key. Of course we could have ‘keys’ mapped in the JSON-LD context to something like https://…#key

As to JWK key use, “auth” is way too general. Both SSH and HTTPSigs use the key for authentication. And they do that using signatures. We need more specific values. SSH is basically for remote shell access, and inbox POST is for federated message passing. I think we need values that express use on this level, to really be clear. We could even just name them “ssh” and “activitypub”.

(I have more thoughts, will write another time)

I discussed with kaniini the keys property will not be a single key but a key chain.
For example you might have different keys for http signature and ssh; both are included in the key chain then.

I am thinking about whether the distinction between the two is really necessary; for example for remote forge access you could just import all keys that can be used for authentication and then authenticate based on them.

@criztovyl ,

Property name: What would be the RDF range of the property though (The order of the keys doesn’t matter, so there’s no need for an ordered rdf:List)

Key use: If there’s no distinction, how does a Person actor say which keys they approve for SSH and which for HTTP Sigs? Those keys could have different expiration times, different key algorithm/size, be generated in difference places… what if you want a separate key for SSH and a separate key for HTTP Sigs, so that if one key is compromised, it doesn’t given an attacker full access both to SSH and ActivityPub? You then need some way to specify the approved use(s) of each key.

I am in a dead end with this in my mind.

I preferred a generic keys list / keychain that contains all the (public) keys for a user. But I think my preference assumption is wrong - I was thinking that keys are just keys, so why not collect everything that is a key; whereas I now begin thinking that a key without context is worthless and as we clearly need context (which key is approved for what), keys better be grouped by that context.

Soooo… hello again sshKey?

The forges I know all allow for multiple keys (e.g. multiple for machines), so may it rather should be an sshKeys.