Skip to content

Commit

Permalink
feat: Add authentication for ACP (#2649)
Browse files Browse the repository at this point in the history
## Relevant issue(s)

Resolves #2017

## Description

This PR adds ACP identity authentication via HTTP.

Notable changes:
- `acp/identity` has been replaced with the `acp.Identity` struct
  - `identity.PrivateKey` is the private key of the identity
  - `identity.PublicKey` is the public key of the identity
  - `identity.Address` is the bech32 formatted address for the identity
  - keys are all `secp256k1`
- `http` can authenticate requests using a jwt bearer token
- ~a random `audience` value is generated on every http server startup~
  - audience must be set to the defradb host name
  - ~api route `/audience` returns the random audience value~
- `http.Client` will create a signed token if an
`acp.PrivateKeyIdentity` is set
  - jwt token subject is the identity public key
- `cli` `--identity` flag is now a hex encoded private key

Todo:
  - [x] ensure acp docs are updated

## Tasks

- [x] I made sure the code is well commented, particularly
hard-to-understand areas.
- [x] I made sure the repository-held documentation is changed
accordingly.
- [x] I made sure the pull request title adheres to the conventional
commit style (the subset used in the project can be found in
[tools/configs/chglog/config.yml](tools/configs/chglog/config.yml)).
- [x] I made sure to discuss its limitations such as threats to
validity, vulnerability to mistake and misuse, robustness to
invalidation of assumptions, resource requirements, ...

## How has this been tested?

`make test`

Specify the platform(s) on which this was tested:
- MacOS
  • Loading branch information
nasdf authored May 31, 2024
1 parent 5829f9d commit c5ce2f3
Show file tree
Hide file tree
Showing 128 changed files with 803 additions and 406 deletions.
67 changes: 50 additions & 17 deletions acp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,31 @@ Here are some valid expression examples. Assuming these `expr` are under a requi
- `expr: owner +reader`
- `expr: owner+reader`


## DAC Usage CLI:

### Authentication

To perform authenticated operations you will need to generate a `secp256k1` key pair.

The command below will generate a new secp256k1 private key and print the 256 bit X coordinate as a hexadecimal value.

```sh
openssl ecparam -name secp256k1 -genkey | openssl ec -text -noout | head -n5 | tail -n3 | tr -d '\n:\ '
```

Copy the private key hex from the output.

```sh
read EC key
e3b722906ee4e56368f581cd8b18ab0f48af1ea53e635e3f7b8acd076676f6ac
```

Use the private key to generate authentication tokens for each request.

```sh
defradb client ... --identity e3b722906ee4e56368f581cd8b18ab0f48af1ea53e635e3f7b8acd076676f6ac
```

### Adding a Policy:

We have in `examples/dpi_policy/user_dpi_policy.yml`:
Expand Down Expand Up @@ -176,14 +198,13 @@ resources:
CLI Command:
```sh
defradb client acp policy add -i cosmos1f2djr7dl9vhrk3twt3xwqp09nhtzec9mdkf70j -f examples/dpi_policy/user_dpi_policy.yml

defradb client acp policy add -f examples/dpi_policy/user_dpi_policy.yml --identity e3b722906ee4e56368f581cd8b18ab0f48af1ea53e635e3f7b8acd076676f6ac
```

Result:
```json
{
"PolicyID": "24ab8cba6d6f0bcfe4d2712c7d95c09dd1b8076ea5a8896476413fd6c891c18c"
"PolicyID": "50d354a91ab1b8fce8a0ae4693de7616fb1d82cfc540f25cfbe11eb0195a5765"
}
```

Expand All @@ -192,7 +213,7 @@ Result:
We have in `examples/schema/permissioned/users.graphql`:
```graphql
type Users @policy(
id: "24ab8cba6d6f0bcfe4d2712c7d95c09dd1b8076ea5a8896476413fd6c891c18c",
id: "50d354a91ab1b8fce8a0ae4693de7616fb1d82cfc540f25cfbe11eb0195a5765",
resource: "users"
) {
name: String
Expand Down Expand Up @@ -230,7 +251,7 @@ Result:
],
"Indexes": [],
"Policy": {
"ID": "24ab8cba6d6f0bcfe4d2712c7d95c09dd1b8076ea5a8896476413fd6c891c18c",
"ID": "50d354a91ab1b8fce8a0ae4693de7616fb1d82cfc540f25cfbe11eb0195a5765",
"ResourceName": "users"
}
}
Expand All @@ -242,7 +263,7 @@ Result:

CLI Command:
```sh
defradb client collection create -i cosmos1f2djr7dl9vhrk3twt3xwqp09nhtzec9mdkf70j --name Users '[{ "name": "SecretShahzad" }, { "name": "SecretLone" }]'
defradb client collection create --name Users '[{ "name": "SecretShahzad" }, { "name": "SecretLone" }]' --identity e3b722906ee4e56368f581cd8b18ab0f48af1ea53e635e3f7b8acd076676f6ac
```

### Create public documents (without identity)
Expand All @@ -255,7 +276,7 @@ defradb client collection create --name Users '[{ "name": "PublicShahzad" }, {
### Get all docIDs without an identity (shows only public):
CLI Command:
```sh
defradb client collection docIDs -i cosmos1f2djr7dl9vhrk3twt3xwqp09nhtzec9mdkf70j
defradb client collection docIDs --identity e3b722906ee4e56368f581cd8b18ab0f48af1ea53e635e3f7b8acd076676f6ac
```

Result:
Expand All @@ -273,7 +294,7 @@ Result:

### Get all docIDs with an identity (shows public and owned documents):
```sh
defradb client collection docIDs -i cosmos1f2djr7dl9vhrk3twt3xwqp09nhtzec9mdkf70j
defradb client collection docIDs --identity e3b722906ee4e56368f581cd8b18ab0f48af1ea53e635e3f7b8acd076676f6ac
```

Result:
Expand All @@ -300,7 +321,7 @@ Result:
### Access the private document (including field names):
CLI Command:
```sh
defradb client collection get -i cosmos1f2djr7dl9vhrk3twt3xwqp09nhtzec9mdkf70j --name Users "bae-a5830219-b8e7-5791-9836-2e494816fc0a"
defradb client collection get --name Users "bae-a5830219-b8e7-5791-9836-2e494816fc0a" --identity e3b722906ee4e56368f581cd8b18ab0f48af1ea53e635e3f7b8acd076676f6ac
```

Result:
Expand All @@ -325,7 +346,7 @@ Error:
### Accessing the private document with wrong identity:
CLI Command:
```sh
defradb client collection get -i cosmos1x25hhksxhu86r45hqwk28dd70qzux3262hdrll --name Users "bae-a5830219-b8e7-5791-9836-2e494816fc0a"
defradb client collection get --name Users "bae-a5830219-b8e7-5791-9836-2e494816fc0a" --identity 4d092126012ebaf56161716018a71630d99443d9d5217e9d8502bb5c5456f2c5
```

Error:
Expand All @@ -336,7 +357,7 @@ Error:
### Update private document:
CLI Command:
```sh
defradb client collection update -i cosmos1f2djr7dl9vhrk3twt3xwqp09nhtzec9mdkf70j --name Users --docID "bae-a5830219-b8e7-5791-9836-2e494816fc0a" --updater '{ "name": "SecretUpdatedShahzad" }'
defradb client collection update --name Users --docID "bae-a5830219-b8e7-5791-9836-2e494816fc0a" --updater '{ "name": "SecretUpdatedShahzad" }' --identity e3b722906ee4e56368f581cd8b18ab0f48af1ea53e635e3f7b8acd076676f6ac
```

Result:
Expand All @@ -352,7 +373,7 @@ Result:
#### Check if it actually got updated:
CLI Command:
```sh
defradb client collection get -i cosmos1f2djr7dl9vhrk3twt3xwqp09nhtzec9mdkf70j --name Users "bae-a5830219-b8e7-5791-9836-2e494816fc0a"
defradb client collection get --name Users "bae-a5830219-b8e7-5791-9836-2e494816fc0a" --identity e3b722906ee4e56368f581cd8b18ab0f48af1ea53e635e3f7b8acd076676f6ac
```

Result:
Expand All @@ -368,7 +389,7 @@ Result:
### Delete private document:
CLI Command:
```sh
defradb client collection delete -i cosmos1f2djr7dl9vhrk3twt3xwqp09nhtzec9mdkf70j --name Users --docID "bae-a5830219-b8e7-5791-9836-2e494816fc0a"
defradb client collection delete --name Users --docID "bae-a5830219-b8e7-5791-9836-2e494816fc0a" --identity e3b722906ee4e56368f581cd8b18ab0f48af1ea53e635e3f7b8acd076676f6ac
```

Result:
Expand All @@ -384,7 +405,7 @@ Result:
#### Check if it actually got deleted:
CLI Command:
```sh
defradb client collection get -i cosmos1f2djr7dl9vhrk3twt3xwqp09nhtzec9mdkf70j --name Users "bae-a5830219-b8e7-5791-9836-2e494816fc0a"
defradb client collection get --name Users "bae-a5830219-b8e7-5791-9836-2e494816fc0a" --identity e3b722906ee4e56368f581cd8b18ab0f48af1ea53e635e3f7b8acd076676f6ac
```

Error:
Expand All @@ -408,9 +429,21 @@ Error:


## DAC Usage HTTP:
HTTP requests work similar to their CLI counter parts, the main difference is that the identity will just be specified within the Auth Header like so: `Authorization: Basic <identity>`.

Note: The `Basic` label will change to `Bearer ` after JWS Authentication Tokens are supported.
### Authentication

To perform authenticated operations you will need to build and sign a JWT token with the following required fields:

- `sub` public key of the identity
- `aud` host name of the defradb api

> The `exp` and `nbf` fields should also be set to short-lived durations.
The JWT must be signed with the `secp256k1` private key of the identity you wish to perform actions as.

The signed token must be set on the `Authorization` header of the HTTP request with the `bearer ` prefix prepended to it.

If authentication fails for any reason a `403` forbidden response will be returned.

## _AAC DPI Rules (coming soon)_
## _AAC Usage: (coming soon)_
Expand Down
63 changes: 40 additions & 23 deletions acp/identity/identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,34 +8,51 @@
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

/*
Package identity provides defradb identity.
*/

package identity

import "github.com/sourcenetwork/immutable"
import (
cosmosSecp256k1 "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
"github.com/cosmos/cosmos-sdk/types"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/sourcenetwork/immutable"
)

// Identity is the unique identifier for an actor.
type Identity string
// None specifies an anonymous actor.
var None = immutable.None[Identity]()

var (
// None is an empty identity.
None = immutable.None[Identity]()
)
// Identity describes a unique actor.
type Identity struct {
// PublicKey is the actor's public key.
PublicKey *secp256k1.PublicKey
// PrivateKey is the actor's private key.
PrivateKey *secp256k1.PrivateKey
// Address is the actor's unique address.
//
// The address is derived from the actor's public key.
Address string
}

// FromPrivateKey returns a new identity using the given private key.
func FromPrivateKey(privateKey *secp256k1.PrivateKey) immutable.Option[Identity] {
pubKey := privateKey.PubKey()
return immutable.Some(Identity{
Address: AddressFromPublicKey(pubKey),
PublicKey: pubKey,
PrivateKey: privateKey,
})
}

// New makes a new identity if the input is not empty otherwise, returns None.
func New(identity string) immutable.Option[Identity] {
// TODO-ACP: There will be more validation once sourcehub gets some utilities.
// Then a validation function would do the validation, will likely do outside this function.
// https://github.com/sourcenetwork/defradb/issues/2358
if identity == "" {
return None
}
return immutable.Some(Identity(identity))
// FromPublicKey returns a new identity using the given public key.
func FromPublicKey(publicKey *secp256k1.PublicKey) immutable.Option[Identity] {
return immutable.Some(Identity{
Address: AddressFromPublicKey(publicKey),
PublicKey: publicKey,
})
}

// String returns the string representation of the identity.
func (i Identity) String() string {
return string(i)
// AddressFromPublicKey returns the unique address of the given public key.
func AddressFromPublicKey(publicKey *secp256k1.PublicKey) string {
pub := cosmosSecp256k1.PubKey{Key: publicKey.SerializeCompressed()}
// conversion from well known types should never cause a panic
return types.MustBech32ifyAddressBytes("cosmos", pub.Address().Bytes())
}
9 changes: 6 additions & 3 deletions cli/acp_policy_add.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ Notes:
- Learn more about [ACP & DPI Rules](/acp/README.md)
Example: add from an argument string:
defradb client acp policy add -i cosmos1f2djr7dl9vhrk3twt3xwqp09nhtzec9mdkf70j '
defradb client acp policy add -i 028d53f37a19afb9a0dbc5b4be30c65731479ee8cfa0c9bc8f8bf198cc3c075f \
'
description: A Valid DefraDB Policy Interface
actor:
Expand All @@ -61,10 +62,12 @@ resources:
'
Example: add from file:
defradb client acp policy add -i cosmos17r39df0hdcrgnmmw4mvu7qgk5nu888c7uvv37y -f policy.yml
defradb client acp policy add -f policy.yml \
-i 028d53f37a19afb9a0dbc5b4be30c65731479ee8cfa0c9bc8f8bf198cc3c075f
Example: add from file, verbose flags:
defradb client acp policy add --identity cosmos1kpw734v54g0t0d8tcye8ee5jc3gld0tcr2q473 --file policy.yml
defradb client acp policy add --file policy.yml \
--identity 028d53f37a19afb9a0dbc5b4be30c65731479ee8cfa0c9bc8f8bf198cc3c075f
Example: add from stdin:
cat policy.yml | defradb client acp policy add -
Expand Down
3 changes: 2 additions & 1 deletion cli/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ Execute queries, add schema types, obtain node info, etc.`,
return setContextDB(cmd)
},
}
cmd.PersistentFlags().StringVarP(&identity, "identity", "i", "", "ACP Identity")
cmd.PersistentFlags().StringVarP(&identity, "identity", "i", "",
"Hex formatted private key used to authenticate with ACP")
cmd.PersistentFlags().Uint64Var(&txID, "tx", 0, "Transaction ID")
return cmd
}
3 changes: 2 additions & 1 deletion cli/collection.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ func MakeCollectionCommand() *cobra.Command {
},
}
cmd.PersistentFlags().Uint64Var(&txID, "tx", 0, "Transaction ID")
cmd.PersistentFlags().StringVarP(&identity, "identity", "i", "", "ACP Identity")
cmd.PersistentFlags().StringVarP(&identity, "identity", "i", "",
"Hex formatted private key used to authenticate with ACP")
cmd.PersistentFlags().StringVar(&name, "name", "", "Collection name")
cmd.PersistentFlags().StringVar(&schemaRoot, "schema", "", "Collection schema Root")
cmd.PersistentFlags().StringVar(&versionID, "version", "", "Collection version ID")
Expand Down
3 changes: 2 additions & 1 deletion cli/collection_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ Example: create from string:
defradb client collection create --name User '{ "name": "Bob" }'
Example: create from string, with identity:
defradb client collection create -i cosmos1f2djr7dl9vhrk3twt3xwqp09nhtzec9mdkf70j --name User '{ "name": "Bob" }'
defradb client collection create --name User '{ "name": "Bob" }' \
-i 028d53f37a19afb9a0dbc5b4be30c65731479ee8cfa0c9bc8f8bf198cc3c075f
Example: create multiple from string:
defradb client collection create --name User '[{ "name": "Alice" }, { "name": "Bob" }]'
Expand Down
3 changes: 2 additions & 1 deletion cli/collection_delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ Example: delete by docID:
defradb client collection delete --name User --docID bae-123
Example: delete by docID with identity:
defradb client collection delete -i cosmos1f2djr7dl9vhrk3twt3xwqp09nhtzec9mdkf70j --name User --docID bae-123
defradb client collection delete --name User --docID bae-123 \
-i 028d53f37a19afb9a0dbc5b4be30c65731479ee8cfa0c9bc8f8bf198cc3c075f
Example: delete by filter:
defradb client collection delete --name User --filter '{ "_gte": { "points": 100 } }'
Expand Down
2 changes: 1 addition & 1 deletion cli/collection_get.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Example:
defradb client collection get --name User bae-123
Example to get a private document we must use an identity:
defradb client collection get -i cosmos1f2djr7dl9vhrk3twt3xwqp09nhtzec9mdkf70j --name User bae-123
defradb client collection get -i 028d53f37a19afb9a0dbc5b4be30c65731479ee8cfa0c9bc8f8bf198cc3c075f --name User bae-123
`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
Expand Down
2 changes: 1 addition & 1 deletion cli/collection_list_doc_ids.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Example: list all docID(s):
defradb client collection docIDs --name User
Example: list all docID(s), with an identity:
defradb client collection docIDs -i cosmos1f2djr7dl9vhrk3twt3xwqp09nhtzec9mdkf70j --name User
defradb client collection docIDs -i 028d53f37a19afb9a0dbc5b4be30c65731479ee8cfa0c9bc8f8bf198cc3c075f --name User
`,
RunE: func(cmd *cobra.Command, args []string) error {
col, ok := tryGetContextCollection(cmd)
Expand Down
2 changes: 1 addition & 1 deletion cli/collection_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Example: update by docID:
--docID bae-123 --updater '{ "verified": true }'
Example: update private docID, with identity:
defradb client collection update -i cosmos1f2djr7dl9vhrk3twt3xwqp09nhtzec9mdkf70j --name User \
defradb client collection update -i 028d53f37a19afb9a0dbc5b4be30c65731479ee8cfa0c9bc8f8bf198cc3c075f --name User \
--docID bae-123 --updater '{ "verified": true }'
`,
Args: cobra.RangeArgs(0, 1),
Expand Down
2 changes: 1 addition & 1 deletion cli/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Do a query request from a file by using the '-f' flag. Example command:
defradb client query -f request.graphql
Do a query request from a file and with an identity. Example command:
defradb client query -i cosmos1f2djr7dl9vhrk3twt3xwqp09nhtzec9mdkf70j -f request.graphql
defradb client query -i 028d53f37a19afb9a0dbc5b4be30c65731479ee8cfa0c9bc8f8bf198cc3c075f -f request.graphql
Or it can be sent via stdin by using the '-' special syntax. Example command:
cat request.graphql | defradb client query -
Expand Down
14 changes: 10 additions & 4 deletions cli/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ package cli

import (
"context"
"encoding/hex"
"encoding/json"
"os"
"path/filepath"
"syscall"

"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"golang.org/x/term"
Expand Down Expand Up @@ -139,12 +141,16 @@ func setContextTransaction(cmd *cobra.Command, txId uint64) error {
}

// setContextIdentity sets the identity for the current command context.
func setContextIdentity(cmd *cobra.Command, identity string) error {
// TODO-ACP: `https://github.com/sourcenetwork/defradb/issues/2358` do the validation here.
if identity == "" {
func setContextIdentity(cmd *cobra.Command, privateKeyHex string) error {
if privateKeyHex == "" {
return nil
}
ctx := db.SetContextIdentity(cmd.Context(), acpIdentity.New(identity))
data, err := hex.DecodeString(privateKeyHex)
if err != nil {
return err
}
privKey := secp256k1.PrivKeyFromBytes(data)
ctx := db.SetContextIdentity(cmd.Context(), acpIdentity.FromPrivateKey(privKey))
cmd.SetContext(ctx)
return nil
}
Expand Down
Loading

0 comments on commit c5ce2f3

Please sign in to comment.