rfc: Attested Authority#7
Conversation
alanshaw
left a comment
There was a problem hiding this comment.
This is a really neat way to communicate the attestation. I didn't think of actually putting it in the signature 👏
| "alg": <bytes: authority's Varsig header for its own key type, e.g. Ed25519+DAG-CBOR> | ||
| }, | ||
| "sig": <bytes: authority's raw signature over the canonical DAG-CBOR encoding of "payload"> | ||
| } |
There was a problem hiding this comment.
This looks a lot like an invocation. I'm curious why we would not just put an invocation here? We'd get to re-use a lot of machinery to encode/decode and validate it, and it would be similar in concept to receipts, which are also invocations, with a /ucan/attest/receipt command.
There was a problem hiding this comment.
Yeah, I had the same thought but didn't pursue it. That might be the best version of this.
| ``` | ||
| { | ||
| "payload": { | ||
| "subject": <string: the attested subject DID>, |
There was a problem hiding this comment.
Why do we need subject repeated here?
There was a problem hiding this comment.
Oh, you're right, it's part of what's hashed in payload_hash as it is. We can scratch it here.
| { | ||
| "payload": { | ||
| "subject": <string: the attested subject DID>, | ||
| "payload_hash": <bytes: SHA-256 hash of the canonical DAG-CBOR encoding of the delegation payload>, |
There was a problem hiding this comment.
Yeah, that's what that should be really.
alanshaw
left a comment
There was a problem hiding this comment.
Happy for this to merge as is. I have left comments for your consideration.
|
|
||
| ## Abstract | ||
|
|
||
| This document proposes a generic scheme for using externally-verified identities as UCAN delegation subjects. Such identities, including email addresses, OAuth-based identities, and others, have no associated keypair under user control, and therefore cannot sign UCAN delegations directly. A trusted authority performs an out-of-band verification (email loop, OAuth exchange, etc.) and produces a cryptographic attestation on behalf of the subject identity. A generic Varsig signature type (`authority-attestation`) is defined that encodes the authority's attestation in place of a conventional asymmetric signature, with the specific verification method encoded in the attestation payload rather than the type. This allows attested identities to appear as `iss` in root UCAN delegations while remaining structurally honest about the nature of the verification performed. |
There was a problem hiding this comment.
| This document proposes a generic scheme for using externally-verified identities as UCAN delegation subjects. Such identities, including email addresses, OAuth-based identities, and others, have no associated keypair under user control, and therefore cannot sign UCAN delegations directly. A trusted authority performs an out-of-band verification (email loop, OAuth exchange, etc.) and produces a cryptographic attestation on behalf of the subject identity. A generic Varsig signature type (`authority-attestation`) is defined that encodes the authority's attestation in place of a conventional asymmetric signature, with the specific verification method encoded in the attestation payload rather than the type. This allows attested identities to appear as `iss` in root UCAN delegations while remaining structurally honest about the nature of the verification performed. | |
| This document proposes a generic scheme for using externally-verified identities as UCAN delegation subjects. Such identities, including email addresses, OAuth-based identities, and others, have no associated keypair under user control, and therefore cannot sign UCAN delegations directly. A trusted authority performs an out-of-band verification (email loop, OAuth exchange, etc.) and produces a cryptographic attestation on behalf of the subject identity. A new Varsig signature type (`authority-attestation`) is defined that encodes the authority's attestation in place of a conventional asymmetric signature, with the specific verification method encoded in the attestation payload rather than the signature type. This allows attested identities to appear as `iss` in root UCAN delegations while remaining structurally honest about the nature of the verification performed. |
|
|
||
| ### 3.1 `did:mailto` | ||
|
|
||
| 1. The client authors a delegation payload in which the `did:mailto` issues some capability to the client's agent identity, and issues an invocation to the authority asking for it to be signed. |
There was a problem hiding this comment.
| 1. The client authors a delegation payload in which the `did:mailto` issues some capability to the client's agent identity, and issues an invocation to the authority asking for it to be signed. | |
| 1. The client authors a payload that describes a delegation in which the `did:mailto` issues some capability to the client's agent identity, and issues an invocation to the authority asking for it to be signed. |
There was a problem hiding this comment.
"Delegation payload" here is meant to refer to a literal payload portion of a UCAN delegation, but it sounds like it's coming across as "a payload which is an entire delegation". Maybe I should make it "the payload portion of a delegation"?
| 1. The client authors a delegation payload in which the `did:mailto` issues some capability to the client's agent identity, and issues an invocation to the authority asking for it to be signed. | ||
| 2. The authority computes a an HMAC over `(delegation_payload, iat, exp)` using a key the authority controls, where `delegation_payload` is the canonical (DAG-CBOR) encoding of the delegation payload, `iat` is the time the link was issued, and `exp` is some expiration time for the link. `iat` is optional, but will result in a timestamp on the final signature for tracking purposes. | ||
| 3. The authority sends an email to the address encoded in the `did:mailto` DID, containing a verification link pointing back to the authority's web server. The URL's params include the delegation payload, the `exp`, and the HMAC, suitably encoded. | ||
| 4. When the user clicks the link, the authority validates the HMAC. |
There was a problem hiding this comment.
This is not how things currently are implemented (we use a signed invocation not HMAC), and I'd probably adjust the spec to allow this process to be done differently, perhaps within some guide rails.
I think you already know, but just incase, there's a description of the current process here: https://hackmd.io/VUuW15CeRI6J1ZIR2yZwpA?view#accessconfirm
There was a problem hiding this comment.
Yeah, I've been thinking this through, and I was sure there was a reason an invocation didn't make sense, but…I don't see it anymore. This section's evolved a bit so maybe I just worked myself into reimplementing a worse version of the invocation. 😅
|
|
||
| The authority MAY define a policy governing what delegations it will verify and attest to, and reject requests which are not permitted by that policy. For instance, the authority may reject delegations without a recent `nbf` ("not before"), or with an `exp` ("expiration") that is `null` or too far in the future. To reject an attestation request, the authority MUST return a failure for the original attestation request invocation. If it does not reject the request, the authority MUST use the delegation payload as given, with no changes. | ||
|
|
||
| The verification link should have a reasonably short expiration. 15–60 minutes is RECOMMENDED. |
There was a problem hiding this comment.
I think 60 minutes is a long time. I would say 10 minutes (or shorter) is usual for this type of thing. It really shouldn't take that long to log into your email nowadays. I'd perhaps rephrase to say something like "recommended to be no longer than 15 minutes".
|
|
||
| The verification link should have a reasonably short expiration. 15–60 minutes is RECOMMENDED. | ||
|
|
||
| The authority MAY use a digest in place of the full payload in the URL, but this requires it to store the pending payload while waiting for verification. In this configuration, the stored payload can be evicted from the store when the link expires. |
There was a problem hiding this comment.
Yeah I think enumerating all the different ways to do this is not necessary - just saying you get to choose how to do this (out of scope) is fine IMHO.
| 0x34 Varsig prefix | ||
| 0x01 Varsig version 1 | ||
| 0x300001 authority-attestation algorithm discriminant (varint) | ||
| 0x71 Payload encoding: DAG-CBOR |
There was a problem hiding this comment.
A previous version of this RFC included the authority DID here. Why would we not do that?
If we continue to not, then there is an earlier mention of it in the intro I think that would need to be removed.
There was a problem hiding this comment.
Oh, shoot, I thought I got them all.
I couldn't come up with a reason it was still useful, but maybe it is? You don't need to know it to interpret and validate the signature. On the other hand, it does correspond with the verification method's content. But back on the first hand, a public key appears in a Multikey verification method, and the key doesn't appear in a corresponding Varsig; you're expected to know the public key you expect already.
| * **`nonce`** is empty. As an assertion of fact, the invocation is inherently idempotent. | ||
| * **`exp`** is `null`. As an assertion of fact, the invocation cannot expire: the signature cannot have not happened because time has passed. The delegation's `exp` controls the expiration of the delegation. |
There was a problem hiding this comment.
Not sure about this. It feels valuable to be able to expire the attestation, but conversely I think "the signature cannot have not happened because time has passed" is a fair point.
Don't oauth access tokens have time bounds that would be useful to echo in exp?
Login session lengths are a thing - logging in again after a certain period of time seems like a good idea...
There was a problem hiding this comment.
Yes, but all of that is the delegation's exp, which is why the service is allowed to constrain what exps are allowed. But the signing still happened either way. It gets a bit weird if you can have a delegation which hasn't expired, but whose signature somehow has. I suppose the closest analog is when a principal rotates keys and invalidates their old signatures, but that's not a fun situation.
|
|
||
| ## 5. Verification | ||
|
|
||
| When a verifier encounters a delegation with a Varsig header with the algorithm discriminant `0x300001`, it should: |
There was a problem hiding this comment.
Just thinking about this, I'd implement as a varsig codec and did verifier resolver in libforge that we use in sprue (and elsewhere). That is to say, I'd not add it to ucantone - we have spec'd this here, but it is by no means canon and is certainly not part of the UCAN/varsig or DID specs.
There was a problem hiding this comment.
That makes sense to me. I also think it makes sense in ucantone, since the verifier resolvers are all registered extensions anyway, not core to the library—they could just as easily each be their own separate modules. But if we're worried about ucantone vouching for it too much by including the implementation in the module, libforge makes sense. The registry architecture makes it simple to move at any time.
_This is part of a set of PRs across the `fil-forge` repos. They all work together using a `go.work`. I'm not sure how to make CI do anything useful, though._ # Attested Signatures + UCAN Principal Clarification This PR (along with coordinated changes in sibling repos) reimplements [attested signatures](fil-one/RFC#7). This was written less thoroughly in [sprue#15](fil-forge/sprue#15), but it needed to be more central for everything in the network to use it. Pulling on that thread unraveled a whole bunch of latent issues with the domain model in UCAN. Nothing fundamentally wrong, just some things that were conflated that are now teased apart to reflect some subtle but important distinctions. ## Attested Signatures To recap: `did:mailto:` DIDs have no native key material. Rather than signing directly, a mailto DID delegates to a trusted **authority** (the signing service), which issues a `/ucan/attest/proof` invocation over a SHA2-256 hash of the message. That invocation *is* the signature. On the verify side, the signature is decoded as an invocation, the hash is checked against the message, and the invocation is validated against the authority. ## DID Documents as First-Class Objects Previously, we "resolved" DIDs directly to verifiers. Now DID documents are a first-class concept in the `did` package, rather than being reimplemented in multiple codebases. The previous code assumed that there was a one-to-one mapping between DIDs and signers/verifiers. That's a natural asssumption when most things are `did:key:`s, where that's true. But in general, a DID document can have multiple `verificationMethod`s, and *that's* what actually maps to a signer/verifier pair. Now we can resolve a DID to a document, and turn a *verification method* within it into a verifier. We can also make a "multi-verifier" (which succeeds if any of them succeed) out of all of the verification methods in the document, or out of just those with a particular verification relationship (notably `CapabilityInvocation` and `CapabilityDelegation`). `did.Document` is now a real typed struct with verification relationships. Resolvers return documents; verifier derivation is a separate step via a pluggable factory registry. This is what makes custom verification method types (like `AuthorityAttestation`) possible. For `did:mailto:`, `didmailto.Resolver` generates synthetic DID documents on-the-fly. Each document contains an `AuthorityAttestation` verification method naming the authority, and the registered factory for that type produces an `AttestedVerifier`. ## UCAN 1.0 Terminology The refactor also tightens up the principal/signing model: - **`Principal`** remains "something with a DID()". - **`Signer`** and **`Verifier`** are no longer `Principal`s. They only deal in signatures. A verification method exists independently of a DID, and a signer or verifier exists independently for the same reason. But they're often tied together, so… - **`Issuer`** is now the noun for "a `Signer` tied to a `Principal`". In many cases, `Issuer` simply replaces the existing use of `Signer`. Notably, there are lot of variables already named `issuer` which were `ucan.Signer` and are now `ucan.Issuer`, which gives me some confidence that this is correct. - **`multikey`** is now a specific family of `Signer`s and `Verifier`s which are based on cryptographic keys which the DID document represents as `Multikey`-type verification methods, specifically Ed25519 and secp256k1 currently. There's a set of enhanced `multikey.Issuer`, `multikey.Signer`, and `multikey.Verifier` types which know about the keys they represent. - **`did:key:`s are not keys** and vice versa. Because of the conflation of signers/verifiers and identities, keys were often represented as the corresponding `did:key:`s. But these are different things: a *key* is a method of signing and verification, while a *DID* is something that identifies a subject. The subject of a `did:key:` is, generally speaking, the entity which holds the private key—which is different from being the key itself! Now that's clearer. - Thus **we no longer "wrap" keys**. Previously, you'd accomplish a `did:web:` signer by creating a `did:key:` signer and wrapping it with a `did:web:` so that it reported that as its DID. Now they're separate concerns. The equivalent of "wrapping" is `multikey.NewIssuer(did.DID, multikey.Signer) multikey.Issuer`. The equivalent of unwrapping is `anIssuer.PrivateKey()`—which returns an actual *key*, not a principal. - We also have `identity.Identity`. This was promoted from `sprue` to solve the same problem in other modules. `Identity` simply wraps an `Issuer` to provide a `DIDDocument()` factory, which can then be used both to serve the DID document on the web and to resolve one's own DID. `Identity` is intended to be used for "our" identity in any given service. The fact that it's such a simple wrapper seems like a smell to me, but it's useful and I didn't want to mess with it too much. It might want a little massaging in the future. ## Questions for Reviewers - For `attested`, the verification needs a context, because it needs to recursively validate the invocation that is the attestation signature. But `Verify()` doesn't take a context right now, so the verifier holds a context given at creation. That's a bit odd. Should `Verify()` change to take a context, or should we keep doing this?
📖 Preview
An attempt to solve the weirdness of how we do attestations, and to extend into OAuth and other systems. I'm going to hold a discussion session to talk through this.
AuthorityAttestationto a new context.did:mailtospecificallycontrollershould be attesting authority.