Super User Account

Summary

With the advancement of Cadence language and Flow Blockchain, I think there is a need to separate AuthAccount into two (maybe more) security levels. Inspiration comes from operating systems security. (sudo from Linux world and administrator account from Windows )

Before writing a FLIP, I would like to get some community feedback.

Basic Idea

The basic idea is to move some dangerous operations ( like adding/removing the public key, deploying a contract, deleting a contract, getting AuthAccount capability, etc.) to SuperAuthAccount

For example below transaction will fail:

transaction(publicKey: [UInt8]) {
    prepare(signer: AuthAccount) {
        let key = PublicKey(
            publicKey: publicKey,
            signatureAlgorithm: SignatureAlgorithm.ECDSA_P256
        )
        signer.keys.add(
            publicKey: key,
            hashAlgorithm: HashAlgorithm.SHA3_256,
            weight: 10.0
        )
    }
}

instead it had to be written as:

For example below transaction will fail:

transaction(publicKey: [UInt8]) {
    prepare(signer: SuperAuthAccount) {
        let key = PublicKey(
            publicKey: publicKey,
            signatureAlgorithm: SignatureAlgorithm.ECDSA_P256
        )
        signer.keys.add(
            publicKey: key,
            hashAlgorithm: HashAlgorithm.SHA3_256,
            weight: 10.0
        )
    }
}

For multi-sign scenarios, there can be a mixture of SuperAuthAccount and AuthAccount in transactions, depending on the access. It is up to wallets to get approval from users. This way, even if wallets don’t support this new feature, they can continue to work as is. ( by signing as SuperAuthAccount without warning, which is equal to today’s case )

With support from wallet developers by enabling some features like:
- Big warning when a transaction wants to use SuperAuthAccount
- Requiring the user to authenticate with a password again before signing

we can prevent some dangerous attacks on users.

Another benefit of this approach is even if I gave AuthAccount access with capability to someone; I can prevent them from updating/deleting contracts on my account or adding/revoking public keys.

Backwards compatibility

Most of the transactions on the network don’t involve these actions, so the impact would be minimal. Some contracts may need to be updated.

Sorry I missed this when you first posted it, @bluesign!

What do you think about implementing this as three different interfaces on AuthAccount: Storage , Capabilities , and Administrative?

The first would be “normal access to ‘stuff’ in your account storage”. (i.e. This transaction can access your things.)

The second would be “creating new Capabilities” (i.e. This transaction can grant ongoing access to a subset of your things.)

The last one would be “adding keys” and “deploying contracts” (and – once implemented – creating AuthAccount Capabilities). (i.e. This transaction can grant ongoing access to ALL of your things.)

So, for a normal transaction, we’d replace:
(signer: AuthAccount) :arrow_right: (signer: AuthAccount{Storage})

And to change a key, you’d need to replace:
(signer: AuthAccount) :arrow_right: (signer: AuthAccount{Administrative}).

I see three options for how to handle legacy transaction templates:

  1. Make AuthAccount (without any interface limitations) invalid, breaking all legacy transactions
  2. Interpret AuthAccount (without any interface limitations) as AuthAccount{Storage}, breaking only transactions that add keys/deploy code
  3. Interpret AuthAccount as unrestricted (the way Cadence typically works), which breaks no legacy transactions, but which leaves a big security hole open

My suggestion would be to implement option 2 temporarily, and move to option 1 in the long run.

(FWIW- @bluesign’s original suggestion would cause breakage similar to Option 2.)

1 Like

Adding interfaces for AuthAccount and allowing/using restricted types in prepare's parameter list had been proposed quite a while ago, it’s great to see more support for this idea now.

I like the idea of grouping the granted access as lined out, but e.g. Administrative seems too broad and not well-defined – e.g. some storage operations might also be “administrative”. We already namespace administrative operations, e.g. key managements as AuthAccount.keys, so maybe it makes sense to have an interfaces for each namespace (e.g. keys, contracts, etc.).

How would this fit in with the currently proposed changes to downcasting? The FLIP proposes to allow arbitrary downcasting, effectively removing restricted types (T{Us}). I guess each of the proposed interfaces which grant access to certain functionality would become an “entitlement”?

Also related is the discussion around owned values vs references that came up in the capability controllers FLIP: Currently AuthAccount is a value type – but actually has reference semantics internally. It might make sense to change uses of AuthAccount to &AuthAccount.

Combining both, one could imagine a signature authorizing the addition of a new key as:
prepare(signer: auth{AuthAccount.KeyManagement} &AuthAccount).

@dete

The first would be “normal access to ‘stuff’ in your account storage”. (i.e. This transaction can access your things.)

The second would be “creating new Capabilities” (i.e. This transaction can grant ongoing access to a subset of your things.)

I think those two are great distinctions, I am just concerned here with “creating new Capabilities” part only, as we came to a state that almost every transaction needs a capability access right now. ( with boilerplate code )

@bastian

How would this fit in with the currently proposed changes to downcasting? The FLIP proposes to allow arbitrary downcasting, effectively removing restricted types (T{Us} ).

I think here all methods will be auth anyway

Currently AuthAccount is a value type – but actually has reference semantics internally. It might make sense to change uses of AuthAccount to &AuthAccount.

this makes totally sense.

1 Like

I think this is a good idea! How would the transaction know what access level to give? Would it require another signature or something, or is it as simple as just declaring the signer type as Super or AuthAccount{Administrative}

Simply declaring signer type is the easiest. Also later can be extended more.

I really like this idea @bluesign!

@bastian I agree on this, but how do you think this change would affect Capabilities on AuthAccounts? And on that note, I hope we’d be able to issue Capabilities on this newly proposed SuperAuthAccount

Right, but given that the signer parameters are currently owned and not references (AuthAccount instead of &AuthAccount), the transaction would be able to access those auth functions.

Building upon https://github.com/onflow/flips/pull/54, i.e. making the parameters references and introducing entitlements for the various administrative operations (like adding a key), should be a good solution.

Agreed! I don’t think there is any need or advantage in allowing/requiring this to be expressed in the transaction object itself, i.e. on the protocol level.
Declaring it in the Cadence code should be sufficient, simpler, and causes fewer breakage.

Borrowing capabilities always result in references, so that would align nicely.

@daniel what do you think of the currently proposed solution of building on https://github.com/onflow/flips/pull/54, i.e. making signer parameters references and introducing entitlements for AuthAccount?

@bluesign would you be OK with me opening a FLIP for this proposal?

that would be great, thank you very much

My understanding of the problem is that a security mechanism has to be included to prevent a potential malicious party from secretly adding additional keys (or deploying a contract) to an existing account.

But:

  • a malicious site can propose transactions to empty an account without adding new keys
  • a user’s keys may be stolen by a malicious party, that would then use them to send transactions that would drain the account without adding fresh keys
  • wallets need to add support for the new sudo mode, but in that case wallets could implement a safe-mode and only allow audited transactions (FLIX)

While thinking through how entitlements for accounts could look like it occurred to me that we do not even need the distinction between AuthAccount and PublicAccount anymore.

Here is an example of how a refactoring a part of the account type could look like:

struct interface ContractAdmin {

    auth(ContractAdmin) let contractAdmin: auth &Account.Contracts
}

// ... other entitlement interfaces granting access to keys, capabilities, etc.

struct Account: ContractAdmin {

    let address: Address

    // ... other fields, functions, and types

    let contracts: &Account.Contracts

    auth(ContractAdmin) let contractAdmin: auth &Account.Contracts

    struct Contracts {

        fun get(name: String): DeployedContract?

        auth fun update__experimental(name: String, code: [UInt8]): DeployedContract

        // ... other fields and functions
    }
}
  • We would only have one type Account
  • Previous uses of PublicAccount would become &Account. Note that the reference is non-auth, meaning that informational fields like address, but also fields like contracts could be accessed and would allow “read-only operations”.
  • Previous uses of AuthAccount would become auth &Account, effectively granting access to all auth fields
  • A signer type auth(ContractAdmin) &Account would only allow access to managing contracts, but not to e.g. managing keys
  • One small downside is that we would need to expose two fields for each nested reference, one non-auth and one auth, e.g. contracts and contractAdmin
  • (Note that naming is just strawman syntax, suggestions welcome)

@sainati Did I apply https://github.com/onflow/flips/pull/54 here correctly?

1 Like

I really like this idea! I’ve advocated for having interfaces to separate out the functionality of AuthAccount before, and this idea I think takes that one step further to its logical conclusion. This would be a great use of the entitlements idea.

Did I apply https://github.com/onflow/flips/pull/54 here correctly?

The exact syntax is a bit up in the air I think right now, but the concept is correctly used, yeah.

Previous uses of AuthAccount would become auth &Account

We may not allow auth (without modifiers) to be used to mean “full access”, as full, unfettered access to the reference is not a great default behavior for this, I think. Instead we might have an “owner” entitlement, or allow the composite type Account itself to be used as an entitlement. So this might instead look like auth(Account) &Account.

One small downside is that we would need to expose two fields for each nested reference, one non-auth and one auth, e.g. contracts and contractAdmin

This suggests it might be useful for us to have parametric entitlements; some way to have the entitlements on a field reference be dependent on the entitlements on the parent reference. This is essentially the proposal that the FLIP uses for attachments, but perhaps we could generalize this further.

Can’t we make this to work somehow without duplicate fields ? We only have references as member types in built-types. Maybe we can simply define a rule for them.

Agreed, it would be nice to avoid having two fields per “nested object” (“administration domain”?).

It would be great to not have special rules for certain types, but to have a general feature to express this.

can something like this work ?

struct Account: ContractAdmin {
    let address: Address
    let contracts: Account.Contracts
    struct Contracts {
        fun get(name: String): DeployedContract?
        auth(ContractAdmin) fun update__experimental(name: String, code: [UInt8]): DeployedContract
    }
}

As far as I understand the FLIP (https://github.com/onflow/flips/pull/54), let contracts: Account.Contracts would give out “owned access”, so no matter if the reference to the account would be auth or not, the field gives out owned access and this allows accessing auth members (e.g. the update function).

Is my understanding correct @sainati?