Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add node identity #3125

Merged
merged 39 commits into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
7caedf3
Remove code duplication
islamaliev Oct 8, 2024
a1ea081
Assign identity to a node
islamaliev Oct 8, 2024
f916707
WIP
islamaliev Oct 8, 2024
d916c33
Return RawIdentity, add test
islamaliev Oct 10, 2024
1238aba
Fix lint
islamaliev Oct 10, 2024
1b92c70
Update docs
islamaliev Oct 10, 2024
61c8fb0
Update mocks
islamaliev Oct 10, 2024
82dc3b2
Minor refactor
islamaliev Oct 10, 2024
30a028f
PR fixup
islamaliev Oct 12, 2024
2bd361a
Polish
islamaliev Oct 12, 2024
940177d
Update mocks
islamaliev Oct 12, 2024
55413a3
PR fixup
islamaliev Oct 15, 2024
5ecf6ab
Polish
islamaliev Oct 15, 2024
a32a1f4
PR fixup
islamaliev Oct 15, 2024
62e38c5
PR fixup
islamaliev Oct 15, 2024
ed20e57
PR fixup
islamaliev Oct 17, 2024
a3396bc
Update docs
islamaliev Oct 17, 2024
3f03aa5
Rename command to node-identity
islamaliev Oct 18, 2024
5b2f935
Add assign-node-identity command
islamaliev Oct 20, 2024
b9ebd23
Update docs
islamaliev Oct 20, 2024
d158f83
Lint fix
islamaliev Oct 20, 2024
ab5dc33
Update mocks
islamaliev Oct 20, 2024
4c73fb4
Create parent command node-identity
islamaliev Oct 20, 2024
5946638
PR fixup
islamaliev Oct 21, 2024
8e39ec3
Merge remote-tracking branch 'upstream/develop' into feat/node-identity
islamaliev Oct 21, 2024
0530ab7
Make identity token updatable
islamaliev Oct 22, 2024
ab3a9ea
Update docs
islamaliev Oct 22, 2024
0e0a252
Fix lint
islamaliev Oct 22, 2024
5b3a5c4
Merge remote-tracking branch 'upstream/develop' into feat/node-identity
islamaliev Oct 22, 2024
a0f173f
Turn 2d array of identities into 1d (WIP)
islamaliev Oct 12, 2024
b6d148b
Add clear distinction between user and node identity
islamaliev Oct 13, 2024
95fc645
Pass ctx explicitly
islamaliev Oct 22, 2024
b154dbe
Remove duration from node's identity
islamaliev Oct 23, 2024
af4e2f9
Remove node-identity assign command
islamaliev Oct 23, 2024
2869d87
Polish
islamaliev Oct 24, 2024
3210fbf
Merge remote-tracking branch 'upstream/develop' into feat/node-identity
islamaliev Oct 24, 2024
e4fc548
Make identityRef optional
islamaliev Oct 25, 2024
7fc8e3d
Rename UserIdentity to ClientIdentity
islamaliev Oct 25, 2024
f3ca301
Merge remote-tracking branch 'upstream/develop' into feat/node-identity
islamaliev Oct 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,16 @@ The following keys are loaded from the keyring on start:

- `peer-key` Ed25519 private key (required)
- `encryption-key` AES-128, AES-192, or AES-256 key (optional)
- `node-identity-key` Secp256k1 private key (optional). This key is used for node's identity.

A secret to unlock the keyring is required on start and must be provided via the `DEFRA_KEYRING_SECRET` environment variable. If a `.env` file is available in the working directory, the secret can be stored there or via a file at a path defined by the `--secret-file` flag.
islamaliev marked this conversation as resolved.
Show resolved Hide resolved

The keys will be randomly generated on the inital start of the node if they are not found.
The keys will be randomly generated on the initial start of the node if they are not found.

Alternatively, to randomly generate the required keys, run the following command:

Node identity is an identity assigned to the node. It is used to exchange encryption keys with other nodes.

```
defradb keyring generate
```
Expand Down
41 changes: 41 additions & 0 deletions acp/identity/context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright 2024 Democratized Data Foundation
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

package identity

import (
"context"

"github.com/sourcenetwork/immutable"
)

// identityContextKey is the key type for ACP identity context values.
type identityContextKey struct{}

// FromContext returns the identity from the given context.
//
// If an identity does not exist `NoIdentity` is returned.
func FromContext(ctx context.Context) immutable.Option[Identity] {
identity, ok := ctx.Value(identityContextKey{}).(Identity)
if ok {
return immutable.Some(identity)
}
return None
}

// WithContext returns a new context with the identity value set.
//
// This will overwrite any previously set identity value.
func WithContext(ctx context.Context, identity immutable.Option[Identity]) context.Context {
if identity.HasValue() {
return context.WithValue(ctx, identityContextKey{}, identity.Value())
}
return context.WithValue(ctx, identityContextKey{}, nil)
}
21 changes: 1 addition & 20 deletions acp/identity/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,9 @@
package identity

import (
"encoding/hex"

"github.com/sourcenetwork/defradb/crypto"
)

// RawIdentity holds the raw bytes that make up an actor's identity.
type RawIdentity struct {
// PrivateKey is a secp256k1 private key that is a 256-bit big-endian
// binary-encoded number, padded to a length of 32 bytes in HEX format.
PrivateKey string

// PublicKey is a compressed 33-byte secp256k1 public key in HEX format.
PublicKey string

// DID is `did:key` key generated from the public key address.
DID string
}

// Generate generates a new identity.
func Generate() (RawIdentity, error) {
privateKey, err := crypto.GenerateSecp256k1()
Expand All @@ -43,9 +28,5 @@ func Generate() (RawIdentity, error) {
return RawIdentity{}, err
}

return RawIdentity{
PrivateKey: hex.EncodeToString(privateKey.Serialize()),
PublicKey: hex.EncodeToString(publicKey.SerializeCompressed()),
DID: did,
}, nil
return newRawIdentity(privateKey, publicKey, did), nil
}
116 changes: 60 additions & 56 deletions acp/identity/identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,72 +50,22 @@
}

// FromPrivateKey returns a new identity using the given private key.
//
// - duration: The [time.Duration] that this identity is valid for.
// - audience: The audience that this identity is valid for. This is required
// by the Defra http client. For example `github.com/sourcenetwork/defradb`
// - authorizedAccount: An account that this identity is authorizing to make
// SourceHub calls on behalf of this actor. This is currently required when
// using SourceHub ACP.
// - skipTokenGeneration: If true, BearerToken will not be set. This parameter is
// provided as generating and signing the token is relatively slow, and only required
// by remote Defra clients (CLI, http), or if using SourceHub ACP.
func FromPrivateKey(
privateKey *secp256k1.PrivateKey,
duration time.Duration,
audience immutable.Option[string],
authorizedAccount immutable.Option[string],
skipTokenGeneration bool,
) (Identity, error) {
// In order to generate a fresh token for this identity, use the [UpdateToken]
func FromPrivateKey(privateKey *secp256k1.PrivateKey) (Identity, error) {
publicKey := privateKey.PubKey()
did, err := DIDFromPublicKey(publicKey)
if err != nil {
return Identity{}, err
}

var signedToken []byte
if !skipTokenGeneration {
subject := hex.EncodeToString(publicKey.SerializeCompressed())
now := time.Now()

jwtBuilder := jwt.NewBuilder()
jwtBuilder = jwtBuilder.Subject(subject)
jwtBuilder = jwtBuilder.Expiration(now.Add(duration))
jwtBuilder = jwtBuilder.NotBefore(now)
jwtBuilder = jwtBuilder.Issuer(did)
jwtBuilder = jwtBuilder.IssuedAt(now)

if audience.HasValue() {
jwtBuilder = jwtBuilder.Audience([]string{audience.Value()})
}

token, err := jwtBuilder.Build()
if err != nil {
return Identity{}, err
}

if authorizedAccount.HasValue() {
err = token.Set(acptypes.AuthorizedAccountClaim, authorizedAccount.Value())
if err != nil {
return Identity{}, err
}
}

signedToken, err = jwt.Sign(token, jwt.WithKey(BearerTokenSignatureScheme, privateKey.ToECDSA()))
if err != nil {
return Identity{}, err
}
}

return Identity{
DID: did,
PrivateKey: privateKey,
PublicKey: publicKey,
BearerToken: string(signedToken),
DID: did,
PrivateKey: privateKey,
PublicKey: publicKey,
}, nil
}

// FromToken constructs a new `Indentity` from a bearer token.
// FromToken constructs a new `Identity` from a bearer token.
func FromToken(data []byte) (Identity, error) {
token, err := jwt.Parse(data, jwt.WithVerify(false))
if err != nil {
Expand Down Expand Up @@ -158,3 +108,57 @@
}
return did.String(), nil
}

// IntoRawIdentity converts an `Identity` into a `RawIdentity`.
func (identity Identity) IntoRawIdentity() RawIdentity {
return newRawIdentity(identity.PrivateKey, identity.PublicKey, identity.DID)
}

// UpdateToken updates the `BearerToken` field of the `Identity`.
//
// - duration: The [time.Duration] that this identity is valid for.
// - audience: The audience that this identity is valid for. This is required
// by the Defra http client. For example `github.com/sourcenetwork/defradb`
// - authorizedAccount: An account that this identity is authorizing to make
// SourceHub calls on behalf of this actor. This is currently required when
// using SourceHub ACP.
func (identity *Identity) UpdateToken(
duration time.Duration,
audience immutable.Option[string],
authorizedAccount immutable.Option[string],
) error {
var signedToken []byte
subject := hex.EncodeToString(identity.PublicKey.SerializeCompressed())
now := time.Now()

jwtBuilder := jwt.NewBuilder()
jwtBuilder = jwtBuilder.Subject(subject)
jwtBuilder = jwtBuilder.Expiration(now.Add(duration))
jwtBuilder = jwtBuilder.NotBefore(now)
jwtBuilder = jwtBuilder.Issuer(identity.DID)
jwtBuilder = jwtBuilder.IssuedAt(now)

if audience.HasValue() {
jwtBuilder = jwtBuilder.Audience([]string{audience.Value()})
}

token, err := jwtBuilder.Build()
if err != nil {
return err
}

Check warning on line 148 in acp/identity/identity.go

View check run for this annotation

Codecov / codecov/patch

acp/identity/identity.go#L147-L148

Added lines #L147 - L148 were not covered by tests

if authorizedAccount.HasValue() {
err = token.Set(acptypes.AuthorizedAccountClaim, authorizedAccount.Value())
if err != nil {
return err
}

Check warning on line 154 in acp/identity/identity.go

View check run for this annotation

Codecov / codecov/patch

acp/identity/identity.go#L153-L154

Added lines #L153 - L154 were not covered by tests
}

signedToken, err = jwt.Sign(token, jwt.WithKey(BearerTokenSignatureScheme, identity.PrivateKey.ToECDSA()))
if err != nil {
return err
}

Check warning on line 160 in acp/identity/identity.go

View check run for this annotation

Codecov / codecov/patch

acp/identity/identity.go#L159-L160

Added lines #L159 - L160 were not covered by tests

identity.BearerToken = string(signedToken)
return nil
}
73 changes: 73 additions & 0 deletions acp/identity/raw_identity.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright 2024 Democratized Data Foundation
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

package identity
islamaliev marked this conversation as resolved.
Show resolved Hide resolved

import (
"encoding/hex"

"github.com/decred/dcrd/dcrec/secp256k1/v4"
)

// RawIdentity holds the raw bytes that make up an actor's identity.
type RawIdentity struct {
// PrivateKey is a secp256k1 private key that is a 256-bit big-endian
// binary-encoded number, padded to a length of 32 bytes in HEX format.
PrivateKey string

// PublicKey is a compressed 33-byte secp256k1 public key in HEX format.
PublicKey string

// DID is `did:key` key generated from the public key address.
DID string
}

// PublicRawIdentity holds the raw bytes that make up an actor's identity that can be shared publicly.
type PublicRawIdentity struct {
// PublicKey is a compressed 33-byte secp256k1 public key in HEX format.
PublicKey string

// DID is `did:key` key generated from the public key address.
DID string
}

func newRawIdentity(privateKey *secp256k1.PrivateKey, publicKey *secp256k1.PublicKey, did string) RawIdentity {
res := RawIdentity{
PublicKey: hex.EncodeToString(publicKey.SerializeCompressed()),
DID: did,
}
if privateKey != nil {
res.PrivateKey = hex.EncodeToString(privateKey.Serialize())
}
return res
}

func (r RawIdentity) Public() PublicRawIdentity {
return PublicRawIdentity{
PublicKey: r.PublicKey,
DID: r.DID,
}
}

// IntoIdentity converts a RawIdentity into an Identity.
func (r RawIdentity) IntoIdentity() (Identity, error) {
privateKeyBytes, err := hex.DecodeString(r.PrivateKey)
if err != nil {
return Identity{}, err
}

Check warning on line 64 in acp/identity/raw_identity.go

View check run for this annotation

Codecov / codecov/patch

acp/identity/raw_identity.go#L60-L64

Added lines #L60 - L64 were not covered by tests

privateKey := secp256k1.PrivKeyFromBytes(privateKeyBytes)

return Identity{
PublicKey: privateKey.PubKey(),
PrivateKey: privateKey,
DID: r.DID,
}, nil

Check warning on line 72 in acp/identity/raw_identity.go

View check run for this annotation

Codecov / codecov/patch

acp/identity/raw_identity.go#L66-L72

Added lines #L66 - L72 were not covered by tests
}
6 changes: 3 additions & 3 deletions cli/acp_relationship_add.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func MakeACPRelationshipAddCommand() *cobra.Command {
Long: `Add new relationship

To share a document (or grant a more restricted access) with another actor, we must add a relationship between the
actor and the document. Inorder to make the relationship we require all of the following:
actor and the document. In order to make the relationship we require all of the following:
1) Target DocID: The docID of the document we want to make a relationship for.
2) Collection Name: The name of the collection that has the Target DocID.
3) Relation Name: The type of relation (name must be defined within the linked policy on collection).
Expand All @@ -52,7 +52,7 @@ Notes:
- ACP must be available (i.e. ACP can not be disabled).
- The target document must be registered with ACP already (policy & resource specified).
- The requesting identity MUST either be the owner OR the manager (manages the relation) of the resource.
- If the specified relation was not granted the miminum DPI permissions (read or write) within the policy,
- If the specified relation was not granted the minimum DPI permissions (read or write) within the policy,
and a relationship is formed, the subject/actor will still not be able to access (read or write) the resource.
- Learn more about [ACP & DPI Rules](/acp/README.md)

Expand All @@ -64,7 +64,7 @@ Example: Let another actor (4d092126012ebaf56161716018a71630d99443d9d5217e9d8502
--actor did:key:z7r8os2G88XXBNBTLj3kFR5rzUJ4VAesbX7PgsA68ak9B5RYcXF5EZEmjRzzinZndPSSwujXb4XKHG6vmKEFG6ZfsfcQn \
--identity e3b722906ee4e56368f581cd8b18ab0f48af1ea53e635e3f7b8acd076676f6ac

Example: Creating a dummy relationship does nothing (from database prespective):
Example: Creating a dummy relationship does nothing (from database perspective):
defradb client acp relationship add \
-c Users \
--docID bae-ff3ceb1c-b5c0-5e86-a024-dd1b16a4261c \
Expand Down
2 changes: 1 addition & 1 deletion cli/acp_relationship_delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func MakeACPRelationshipDeleteCommand() *cobra.Command {
Long: `Delete relationship

To revoke access to a document for an actor, we must delete the relationship between the
actor and the document. Inorder to delete the relationship we require all of the following:
actor and the document. In order to delete the relationship we require all of the following:

1) Target DocID: The docID of the document we want to delete a relationship for.
2) Collection Name: The name of the collection that has the Target DocID.
Expand Down
1 change: 1 addition & 0 deletions cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ func NewDefraCommand() *cobra.Command {
MakePurgeCommand(),
MakeDumpCommand(),
MakeRequestCommand(),
MakeNodeIdentityCommand(),
schema,
acp,
view,
Expand Down
Loading
Loading