Why do Blockstack's use of JWT diverge from standards?

@wanderingbort asks:

Most of the JWT examples I’ve seen from profiles/proofs/auth etc are using non-standard and/or verbose claim names instead of the terse registered claim names that are part of the JWT/JOSE standard. What was the motivation for diverging from standard claims?

cc @ryan

As and example, I would have expected the JWT Auth response to decode to something like this:

    {
      header: {
        typ: 'JWT',
        alg: 'ES256'
      },
      payload: {
        iss: 'ryan.id',
        kty: 'blockstack',
        kid: '03fdd57adec3d438ea237fe46b33ee1e016eda6b585c3e27ea66686c2ea5358479',
        pkc: 'xpub661MyMwAqRbcFQVrQr4Q4kPjaP4JjWaf39fBVKjPdK6oGBayE46GAmKzo5UDPQdLSM9DufZiP8eauy56XNuHicBySvZp7J5wsyQVpi2axzZ',
        cp: 'bd62885ec3f0e3838043115f4ce25eedd22cc86711803fb0c19601eeef185e39',
        iat: 1444259422196,
        jti: '0b42722b-e781-434a-805d-c09c476e86b9'
      },
      signature: 'XXXXXXXXX'
    }

Please note, I’ve taken liberties in naming claims that had no good analogue trying to keep in the spirit of the flat and terse representation that standard JWTs have. I have also added some information that would allow a standard JOSE library the chance to understand the JWT and throw a meaningful error message like Failed to validate signature: Unrecognized key type ("blockstack") in token issued by "ryan.id"

1 Like

@wanderingbort we decided to deviate because:

  • we have no concerns about space with our JWTs
  • the JWT claim names are hard to understand and are not self-documenting
  • Blockstack JWTs are used in a very different context from other JWTs and so don’t need to be readable by pieces of software that don’t already expect them

Now, we could change “issuedAt” to the standardized “iat” and add “kty” as a shorthand for “keyType”, but I do think it adds friction to developers getting started and looking to learn how the system works.

Considering that (a) we have no need to use the same claim names AND (b) we have an opportunity to significantly improve the developer user experience and reduce the developer learning curve, I would say we should stick with verbose claim names.

1 Like

Nobody needs to comply with standards per say :innocent:.

However, consider that regardless of the similarities to JWTs, these are not compliant JWTs; they are a custom JSON based format. This increases the burden of developing alternatives to the official BlockStack application libraries as those implementations may not be able to use existing JWT libraries.

While this may have reduced value for the languages with officially maintained libraries, consider that a rich application ecosystem will demand bindings in several popular and fringe languages. The cost of community support for language bindings is marginally higher for every non-standard inclusion.

While the learning curve of JWTs can be amortized over any application, platform or standard that uses them the learning curve of BlockStack’s Tokens is paid in full as part of developer on-boarding for BlockStack.

From the JWT standards document: http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html#rfc.section.4.1

Registered claim names:

  • iss
  • sub
  • aud
  • exp
  • nbf
  • iat
  • jti

Note that the use of all claims is optional. Using zero of these claims results in a valid JWT. A JWT is also a special case of a JWS, where the difference is the JWT has certain standardized fields, especially the header fields like “typ” and “alg”.

Going through all the claims…

  • “iat” and “exp” and “nbf” are all standardized as times.
  • “jti” is not standardized but it is expected to be a unique string.
  • “iss” and “sub” and “aud” all have application-specific processing, where the first two are either strings or URIs and the last one is a list of strings or URIs.

As far as I can tell, “kty”, “kid”, “pkc” and “cp” are not standard claim names.

From this, one could make a strong case for keeping “iss”, “exp”, “nbf”, and “jti”, and one could make a moderate case for keeping “iss” and “sub” and “aud”.

Meanwhile, there’s a very weak case for using other non-standard claim names. Thus, new ones should be created and in order to ensure a good developer UX, they should be verbose and self-documenting, like “publicKey” and “chainPath”.

I’ll also point out that we’re using SECP256k1 as the default curve, which is a non-standard curve. “ES256” is SECP256r1 and so we decided to refer to SECP256k1 as “ES256K”. This isn’t that clean but would love your thoughts on it.

This is reversed but I suspect just a typo, JWS is a special version of a JWT which registers some meaningful claims. As you have alluded to the JWS claims are largely in the header and I will admit to not having taken that in to account properly with my first suggestion.

They are standardized in JWK and JWS. As your token includes key data in the payload, I took the editorial liberty of making it a compatible JWK. There are other claims that disambiguate or identify keys but this is the one which is the least restrictive. Valid values for “kty” are defined in JWA and “blockstack” is surely absent from that list as is any HD wallet-comaptible key system.

“pkc” and “cp” were my “spirit of the standard” swipes at “publicKeyChain” and “chainPath” though, I am not opposed to custom claims being verbose and self descriptive as you suggest. There is optional guidance in JWT#4.2 but i have rarely actually seen people comply to it and so long as these claims do not collide with registered claims there is no compatibility concern with other libraries.

I think this is a fine solution with one caveat, there is a process for registering ES256K officially and doing so helps legitimize it for use in third-party libraries. It may have already been tried and rejected but, I would be happy to help support the official registration of ES256K in any way I can.

The same goes for a “blockstack” or “HD” key type which would in turn officially register “publicKeyChain” and “chainPath” claims.

Thinking this direction would make it easier to one day bring the standard up to your level.

For now, we’ve narrowed the scope of supporting BlockStack in a third party JWT/JOSE library to adding a well-known but technically non-standard-JWA curve for signing/encrypting and a non-standard key type which is more like a key validation for a standard key type.


On second blush perhaps this is a better standard compliant token:

    {
      header: {
        typ: 'JWT',
        alg: 'ES256K',
        jwk: {
          kty: 'blockstack',
          publicKey: '03fdd57adec3d438ea237fe46b33ee1e016eda6b585c3e27ea66686c2ea5358479',
          publicKeyChain: 'xpub661MyMwAqRbcFQVrQr4Q4kPjaP4JjWaf39fBVKjPdK6oGBayE46GAmKzo5UDPQdLSM9DufZiP8eauy56XNuHicBySvZp7J5wsyQVpi2axzZ',
          chainPath: 'bd62885ec3f0e3838043115f4ce25eedd22cc86711803fb0c19601eeef185e39'
        }
      },
      payload: {
        iss: 'ryan.id',
        iat: 1444259422196,
        jti: '0b42722b-e781-434a-805d-c09c476e86b9'
      },
      signature: 'XXXXXXXXX'
    }

I apologize if I am pressing this issue too hard. I mean no disrespect and my efforts are focused on making projects like blockstack widely accepted/accessible (even if my opinions sometimes seem otherwise).

2 Likes

Awesome thanks @wanderingbort.

I put this together as a crack at a JWT for authentication, based in part on what we discussed:

I’ll respond to your other points in a sec.

Would also love @greg’s thoughts as he commented on this thread and has some interesting things to say about curve choices.

@wanderingbort here’s one way to structure the token:

{
  "header": {
    "typ": "JWT",
    "alg": "ES256K"
  },
  "payload": {
    "jti": "0b42722b-e781-434a-805d-c09c476e86b9",
    "iat": 1482268876495,
    "exp": 1482268876495,
    "sub": "bsk:17bWfXcFNNhG8Nu6KZopT17KZGPXxjiA9n:0",
    "iss": "bsk:17bWfXcFNNhG8Nu6KZopT17KZGPXxjiA9n:0",
    "name": "ryan.id",
    "keys": [{
      "kty": "EC",
      "crv": "SECP256k1",
      "pub": "03d03f2c3f7e3cf225757cb303ca91d5a243ad9057485ce2e39fe47b1159bd8c9a",
      "use": "sig",
      "kid": "0"
    }]
  },
  "signature": "XXXXXXXXX"
}

My only thoughts are just that there is no shame in going with “non-standard” curves when “non-standard” means “widely approved by well respected cryptographers and not the hacks at NIST.” :stuck_out_tongue:

1 Like

My response here would be that while I totally understand the desire to make properties/names human friendly (and they probably should specify a dual standard set for this reason), I know folks at MSFT will bark at me if we don’t use the standard ones in our implementations/code.

In my view, standards exist to be enabling not disabling. Even if the door was closed for non-standard algorithms, I would agree there is no shame in using one when it is better than standard alternatives. When the technology moves, standards must/will follow. That said, there is also no shame in participation in making the standards better by trying to achieve official support algorithms “widely approved by well respected cryptographers”

@ryan
is bsk:17bWfXcFNNhG8Nu6KZopT17KZGPXxjiA9n:0 a format you have documented somewhere? is it "blockstack key":<ripmd-160 txid>:<output index> or "blockstack key":<bitcoin address>:<other index> ?

1 Like

Amen to that! :thumbsup:

2 Likes

@larry and I actually came up with this format last night.

It’s:

"blockstack key":<bitcoin-address>:<name-index>

…where the “name index” translates to an index in the list of keys ever owned by this address.

Here’s how it should be interpreted:

  • If the name index (N) is “-1” or absent:
    • The signing/authentication address is equal to the bitcoin address.
  • If the name index (N) is >= 0:
    • The name is the Nth name ever owned by the address.
    • Walk the name transfer operations of the name. Stop if you see a delete operation. The final address that currently owns the name is the signing/authentication address.

This format was created to solve a few issues:

  • A user should be able to create a unique identifier without ever needing to spend coins or send a transaction on the blockchain.
  • A user about to register a name should be able to use a unique identifier immediately while waiting for the name registration to go through.
  • A user should be able to change addresses/keysets without having to get a unique identifier (and thus start from scratch with app accounts). Put another way, the identifier should be long-lived and should be able to survive compromises of the original keys.

As an added bonus feature, we could even allow users to both rotate keys and update usernames. If a name was sent to an address and then deleted and a new key was registered after that, the ID could be transferred to the new key.

Most systems struggle with being able to have all the following 3 properties for IDs:

  • Globally unique
  • Decentralized
  • Key-rotation friendly

Take a look at existing systems:

  • Centralized systems can allow for key changes and name changes because they can keep a centralized DB of UUIDs.
  • PGP does not have long-lived identifiers that can survive key changes.

We’re in a very interesting position here because we can offer globally unique IDs in a decentralized way, without requiring money to be spent, without requiring users to wait for hours, and optionally with support for username updating.

5 Likes

I like it.

While it is off-topic for this thread, If I could inject a 4th tenant (for which I have no solution) for consideration: Pairwise Pseudonymity Friendly. Imagine I want to log in to Apps A and B with my blockstack ID in a way that these apps cannot cross-reference data about me without my permission. Obviously, I wouldn’t be sharing a full-profile or user name with then in this case, just what they “need to know”.

OpenID optionally defines this for their federated model and it is another feature trivially enabled by centralized DBs that contain sensitive info and harder for decentralized identities. I know several vendors that consider pairwise identifiers the default for privacy reasons.

Being a junior developer myself and new to blockstack, I personally would find verbose naming conventions helpful, but if it is the case to make the experience of the developer great without deviating from standard conventions, I find very good documentation helpful for this.

1 Like