Skip to content

Commit

Permalink
feat: Add node identity (#3125)
Browse files Browse the repository at this point in the history
## Relevant issue(s)

Resolves #2908

## Description

Assign an identity to the node upon startup.
  • Loading branch information
islamaliev authored Oct 25, 2024
1 parent bb0917e commit d95c51f
Show file tree
Hide file tree
Showing 124 changed files with 1,607 additions and 1,166 deletions.
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.

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 @@ type Identity struct {
}

// 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 @@ func didFromPublicKey(publicKey *secp256k1.PublicKey, producer didProducer) (str
}
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
}

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

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

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

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
}

privateKey := secp256k1.PrivKeyFromBytes(privateKeyBytes)

return Identity{
PublicKey: privateKey.PubKey(),
PrivateKey: privateKey,
DID: r.DID,
}, nil
}
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

0 comments on commit d95c51f

Please sign in to comment.