Feat/Add Crypto Libs #3

Merged
pn merged 35 commits from feat/add-crypto-libs into main 2026-01-10 21:59:18 +00:00
5 changed files with 1199 additions and 0 deletions
Showing only changes of commit ec87d579aa - Show all commits

View File

@@ -0,0 +1,271 @@
package ucan
import (
"fmt"
"time"
"github.com/MetaMask/go-did-it"
"github.com/MetaMask/go-did-it/crypto"
"github.com/ipfs/go-cid"
"github.com/ucan-wg/go-ucan/pkg/command"
"github.com/ucan-wg/go-ucan/pkg/policy"
"github.com/ucan-wg/go-ucan/token/delegation"
)
// DelegationBuilder provides a fluent API for creating UCAN delegations.
// A delegation grants authority from issuer to audience to perform a command on a subject.
type DelegationBuilder struct {
issuer did.DID
audience did.DID
subject did.DID
cmd command.Command
pol policy.Policy
opts []delegation.Option
err error
isPowerline bool
}
// NewDelegationBuilder creates a builder for constructing UCAN delegations.
func NewDelegationBuilder() *DelegationBuilder {
return &DelegationBuilder{
pol: policy.Policy{},
opts: make([]delegation.Option, 0),
}
}
// Issuer sets the delegation issuer (the entity granting authority).
func (b *DelegationBuilder) Issuer(iss did.DID) *DelegationBuilder {
b.issuer = iss
return b
}
// IssuerString parses and sets the issuer from a DID string.
func (b *DelegationBuilder) IssuerString(issStr string) *DelegationBuilder {
iss, err := did.Parse(issStr)
if err != nil {
b.err = fmt.Errorf("invalid issuer DID: %w", err)
return b
}
b.issuer = iss
return b
}
// Audience sets the delegation audience (the entity receiving authority).
func (b *DelegationBuilder) Audience(aud did.DID) *DelegationBuilder {
b.audience = aud
return b
}
// AudienceString parses and sets the audience from a DID string.
func (b *DelegationBuilder) AudienceString(audStr string) *DelegationBuilder {
aud, err := did.Parse(audStr)
if err != nil {
b.err = fmt.Errorf("invalid audience DID: %w", err)
return b
}
b.audience = aud
return b
}
// Subject sets the delegation subject (the resource being delegated).
// For root delegations, subject should equal issuer.
// For powerline delegations, use Powerline() instead.
func (b *DelegationBuilder) Subject(sub did.DID) *DelegationBuilder {
b.subject = sub
b.isPowerline = false
return b
}
// SubjectString parses and sets the subject from a DID string.
func (b *DelegationBuilder) SubjectString(subStr string) *DelegationBuilder {
sub, err := did.Parse(subStr)
if err != nil {
b.err = fmt.Errorf("invalid subject DID: %w", err)
return b
}
b.subject = sub
b.isPowerline = false
return b
}
// Powerline creates a powerline delegation (subject = nil).
// Powerline automatically delegates all future delegations regardless of subject.
// Use with caution - this is a very powerful pattern.
func (b *DelegationBuilder) Powerline() *DelegationBuilder {
b.subject = nil
b.isPowerline = true
return b
}
// AsRoot sets the subject equal to the issuer (root delegation).
// Root delegations are typically used to create the initial grant of authority.
func (b *DelegationBuilder) AsRoot() *DelegationBuilder {
b.subject = b.issuer
b.isPowerline = false
return b
}
// Command sets the command being delegated.
func (b *DelegationBuilder) Command(cmd command.Command) *DelegationBuilder {
b.cmd = cmd
return b
}
// CommandString parses and sets the command from a string.
func (b *DelegationBuilder) CommandString(cmdStr string) *DelegationBuilder {
cmd, err := command.Parse(cmdStr)
if err != nil {
b.err = fmt.Errorf("invalid command: %w", err)
return b
}
b.cmd = cmd
return b
}
// Policy sets the policy constraints on the delegation.
func (b *DelegationBuilder) Policy(pol policy.Policy) *DelegationBuilder {
b.pol = pol
return b
}
// ExpiresAt sets when the delegation expires.
func (b *DelegationBuilder) ExpiresAt(exp time.Time) *DelegationBuilder {
b.opts = append(b.opts, delegation.WithExpiration(exp))
return b
}
// ExpiresIn sets the delegation to expire after a duration from now.
func (b *DelegationBuilder) ExpiresIn(d time.Duration) *DelegationBuilder {
b.opts = append(b.opts, delegation.WithExpirationIn(d))
return b
}
// NotBefore sets when the delegation becomes valid.
func (b *DelegationBuilder) NotBefore(nbf time.Time) *DelegationBuilder {
b.opts = append(b.opts, delegation.WithNotBefore(nbf))
return b
}
// NotBeforeIn sets the delegation to become valid after a duration from now.
func (b *DelegationBuilder) NotBeforeIn(d time.Duration) *DelegationBuilder {
b.opts = append(b.opts, delegation.WithNotBeforeIn(d))
return b
}
// Meta adds metadata to the delegation.
func (b *DelegationBuilder) Meta(key string, value any) *DelegationBuilder {
b.opts = append(b.opts, delegation.WithMeta(key, value))
return b
}
// Nonce sets a custom nonce. If not called, a random 12-byte nonce is generated.
func (b *DelegationBuilder) Nonce(nonce []byte) *DelegationBuilder {
b.opts = append(b.opts, delegation.WithNonce(nonce))
return b
}
// Build creates the delegation token.
func (b *DelegationBuilder) Build() (*delegation.Token, error) {
if b.err != nil {
return nil, b.err
}
if b.issuer == nil {
return nil, fmt.Errorf("issuer is required")
}
if b.audience == nil {
return nil, fmt.Errorf("audience is required")
}
if b.cmd == "" {
return nil, fmt.Errorf("command is required")
}
if b.isPowerline {
return delegation.Powerline(b.issuer, b.audience, b.cmd, b.pol, b.opts...)
}
// If subject not set and not powerline, default to root delegation
if b.subject == nil {
b.subject = b.issuer
}
return delegation.New(b.issuer, b.audience, b.cmd, b.pol, b.subject, b.opts...)
}
// BuildSealed creates and signs the delegation, returning DAG-CBOR bytes and CID.
func (b *DelegationBuilder) BuildSealed(privKey crypto.PrivateKeySigningBytes) ([]byte, cid.Cid, error) {
tkn, err := b.Build()
if err != nil {
return nil, cid.Cid{}, err
}
return tkn.ToSealed(privKey)
}
// --- Sonr-specific Delegation Builders ---
// NewVaultDelegation creates a delegation for vault operations.
// cmd should be one of: VaultRead, VaultWrite, VaultSign, VaultExport, VaultImport, VaultDelete, VaultAdmin, or Vault (all).
func NewVaultDelegation(issuer, audience did.DID, cmd command.Command, vaultCID string, opts ...delegation.Option) (*delegation.Token, error) {
pol := VaultPolicy(vaultCID)
return delegation.Root(issuer, audience, cmd, pol, opts...)
}
// NewVaultReadDelegation creates a delegation to read from a specific vault.
func NewVaultReadDelegation(issuer, audience did.DID, vaultCID string, opts ...delegation.Option) (*delegation.Token, error) {
return NewVaultDelegation(issuer, audience, VaultRead, vaultCID, opts...)
}
// NewVaultWriteDelegation creates a delegation to write to a specific vault.
func NewVaultWriteDelegation(issuer, audience did.DID, vaultCID string, opts ...delegation.Option) (*delegation.Token, error) {
return NewVaultDelegation(issuer, audience, VaultWrite, vaultCID, opts...)
}
// NewVaultSignDelegation creates a delegation to sign with keys in a specific vault.
func NewVaultSignDelegation(issuer, audience did.DID, vaultCID string, opts ...delegation.Option) (*delegation.Token, error) {
return NewVaultDelegation(issuer, audience, VaultSign, vaultCID, opts...)
}
// NewVaultFullAccessDelegation creates a delegation for all vault operations on a specific vault.
func NewVaultFullAccessDelegation(issuer, audience did.DID, vaultCID string, opts ...delegation.Option) (*delegation.Token, error) {
return NewVaultDelegation(issuer, audience, Vault, vaultCID, opts...)
}
// NewDIDDelegation creates a delegation for DID operations.
// cmd should be one of: DIDCreate, DIDUpdate, DIDDeactivate, or DID (all).
func NewDIDDelegation(issuer, audience did.DID, cmd command.Command, didPattern string, opts ...delegation.Option) (*delegation.Token, error) {
pol := DIDPolicy(didPattern)
return delegation.Root(issuer, audience, cmd, pol, opts...)
}
// NewDIDUpdateDelegation creates a delegation to update DIDs matching a pattern.
func NewDIDUpdateDelegation(issuer, audience did.DID, didPattern string, opts ...delegation.Option) (*delegation.Token, error) {
return NewDIDDelegation(issuer, audience, DIDUpdate, didPattern, opts...)
}
// NewDIDFullAccessDelegation creates a delegation for all DID operations on DIDs matching a pattern.
func NewDIDFullAccessDelegation(issuer, audience did.DID, didPattern string, opts ...delegation.Option) (*delegation.Token, error) {
return NewDIDDelegation(issuer, audience, DID, didPattern, opts...)
}
// NewDWNDelegation creates a delegation for DWN (Decentralized Web Node) operations.
// cmd should be one of: DWNRecordsWrite, DWNRecordsRead, DWNRecordsDelete, or DWN (all).
func NewDWNDelegation(issuer, audience did.DID, cmd command.Command, recordType string, opts ...delegation.Option) (*delegation.Token, error) {
pol := RecordTypePolicy(recordType)
return delegation.Root(issuer, audience, cmd, pol, opts...)
}
// NewDWNReadDelegation creates a delegation to read DWN records of a specific type.
func NewDWNReadDelegation(issuer, audience did.DID, recordType string, opts ...delegation.Option) (*delegation.Token, error) {
return NewDWNDelegation(issuer, audience, DWNRecordsRead, recordType, opts...)
}
// NewDWNWriteDelegation creates a delegation to write DWN records of a specific type.
func NewDWNWriteDelegation(issuer, audience did.DID, recordType string, opts ...delegation.Option) (*delegation.Token, error) {
return NewDWNDelegation(issuer, audience, DWNRecordsWrite, recordType, opts...)
}
// NewRootCapabilityDelegation creates a root delegation granting all capabilities.
// Use with extreme caution - this grants full authority.
func NewRootCapabilityDelegation(issuer, audience did.DID, opts ...delegation.Option) (*delegation.Token, error) {
return delegation.Root(issuer, audience, Root, EmptyPolicy(), opts...)
}

View File

@@ -0,0 +1,259 @@
package ucan
import (
"fmt"
"time"
"github.com/MetaMask/go-did-it"
"github.com/MetaMask/go-did-it/crypto"
"github.com/ipfs/go-cid"
"github.com/ucan-wg/go-ucan/pkg/command"
"github.com/ucan-wg/go-ucan/token/invocation"
)
// InvocationBuilder provides a fluent API for creating UCAN invocations.
// An invocation exercises a delegated capability to perform an action.
type InvocationBuilder struct {
issuer did.DID
subject did.DID
cmd command.Command
proofs []cid.Cid
opts []invocation.Option
err error
}
// NewInvocationBuilder creates a builder for constructing UCAN invocations.
func NewInvocationBuilder() *InvocationBuilder {
return &InvocationBuilder{
proofs: make([]cid.Cid, 0),
opts: make([]invocation.Option, 0),
}
}
// Issuer sets the invocation issuer (the entity invoking the capability).
func (b *InvocationBuilder) Issuer(iss did.DID) *InvocationBuilder {
b.issuer = iss
return b
}
// IssuerString parses and sets the issuer from a DID string.
func (b *InvocationBuilder) IssuerString(issStr string) *InvocationBuilder {
iss, err := did.Parse(issStr)
if err != nil {
b.err = fmt.Errorf("invalid issuer DID: %w", err)
return b
}
b.issuer = iss
return b
}
// Subject sets the invocation subject (the resource being acted upon).
func (b *InvocationBuilder) Subject(sub did.DID) *InvocationBuilder {
b.subject = sub
return b
}
// SubjectString parses and sets the subject from a DID string.
func (b *InvocationBuilder) SubjectString(subStr string) *InvocationBuilder {
sub, err := did.Parse(subStr)
if err != nil {
b.err = fmt.Errorf("invalid subject DID: %w", err)
return b
}
b.subject = sub
return b
}
// Command sets the command being invoked.
func (b *InvocationBuilder) Command(cmd command.Command) *InvocationBuilder {
b.cmd = cmd
return b
}
// CommandString parses and sets the command from a string.
func (b *InvocationBuilder) CommandString(cmdStr string) *InvocationBuilder {
cmd, err := command.Parse(cmdStr)
if err != nil {
b.err = fmt.Errorf("invalid command: %w", err)
return b
}
b.cmd = cmd
return b
}
// Proof adds a delegation CID to the proof chain.
// Proofs should be ordered from leaf (matching invocation's issuer as audience)
// to root delegation.
func (b *InvocationBuilder) Proof(delegationCID cid.Cid) *InvocationBuilder {
b.proofs = append(b.proofs, delegationCID)
return b
}
// ProofString parses and adds a delegation CID from a string.
func (b *InvocationBuilder) ProofString(cidStr string) *InvocationBuilder {
c, err := cid.Parse(cidStr)
if err != nil {
b.err = fmt.Errorf("invalid proof CID: %w", err)
return b
}
b.proofs = append(b.proofs, c)
return b
}
// Proofs sets all proofs at once, replacing any previously added.
func (b *InvocationBuilder) Proofs(cids []cid.Cid) *InvocationBuilder {
b.proofs = cids
return b
}
// Arg adds an argument to the invocation.
func (b *InvocationBuilder) Arg(key string, value any) *InvocationBuilder {
b.opts = append(b.opts, invocation.WithArgument(key, value))
return b
}
// Audience sets the invocation's audience (executor if different from subject).
func (b *InvocationBuilder) Audience(aud did.DID) *InvocationBuilder {
b.opts = append(b.opts, invocation.WithAudience(aud))
return b
}
// AudienceString parses and sets the audience from a DID string.
func (b *InvocationBuilder) AudienceString(audStr string) *InvocationBuilder {
aud, err := did.Parse(audStr)
if err != nil {
b.err = fmt.Errorf("invalid audience DID: %w", err)
return b
}
b.opts = append(b.opts, invocation.WithAudience(aud))
return b
}
// ExpiresAt sets when the invocation expires.
func (b *InvocationBuilder) ExpiresAt(exp time.Time) *InvocationBuilder {
b.opts = append(b.opts, invocation.WithExpiration(exp))
return b
}
// ExpiresIn sets the invocation to expire after a duration from now.
func (b *InvocationBuilder) ExpiresIn(d time.Duration) *InvocationBuilder {
b.opts = append(b.opts, invocation.WithExpirationIn(d))
return b
}
// Meta adds metadata to the invocation.
func (b *InvocationBuilder) Meta(key string, value any) *InvocationBuilder {
b.opts = append(b.opts, invocation.WithMeta(key, value))
return b
}
// Nonce sets a custom nonce. If not called, a random 12-byte nonce is generated.
func (b *InvocationBuilder) Nonce(nonce []byte) *InvocationBuilder {
b.opts = append(b.opts, invocation.WithNonce(nonce))
return b
}
// EmptyNonce sets an empty nonce for idempotent operations.
func (b *InvocationBuilder) EmptyNonce() *InvocationBuilder {
b.opts = append(b.opts, invocation.WithEmptyNonce())
return b
}
// Cause sets the receipt CID that enqueued this invocation.
func (b *InvocationBuilder) Cause(receiptCID cid.Cid) *InvocationBuilder {
b.opts = append(b.opts, invocation.WithCause(&receiptCID))
return b
}
// Build creates the invocation token.
func (b *InvocationBuilder) Build() (*invocation.Token, error) {
if b.err != nil {
return nil, b.err
}
if b.issuer == nil {
return nil, fmt.Errorf("issuer is required")
}
if b.subject == nil {
return nil, fmt.Errorf("subject is required")
}
if b.cmd == "" {
return nil, fmt.Errorf("command is required")
}
return invocation.New(b.issuer, b.cmd, b.subject, b.proofs, b.opts...)
}
// BuildSealed creates and signs the invocation, returning DAG-CBOR bytes and CID.
func (b *InvocationBuilder) BuildSealed(privKey crypto.PrivateKeySigningBytes) ([]byte, cid.Cid, error) {
tkn, err := b.Build()
if err != nil {
return nil, cid.Cid{}, err
}
return tkn.ToSealed(privKey)
}
// --- Sonr-specific Invocation Builders ---
// VaultReadInvocation creates an invocation to read from a vault.
func VaultReadInvocation(issuer, subject did.DID, proofs []cid.Cid, vaultCID string, opts ...invocation.Option) (*invocation.Token, error) {
allOpts := append([]invocation.Option{invocation.WithArgument("vault", vaultCID)}, opts...)
return invocation.New(issuer, VaultRead, subject, proofs, allOpts...)
}
// VaultWriteInvocation creates an invocation to write to a vault.
func VaultWriteInvocation(issuer, subject did.DID, proofs []cid.Cid, vaultCID string, data any, opts ...invocation.Option) (*invocation.Token, error) {
allOpts := append([]invocation.Option{
invocation.WithArgument("vault", vaultCID),
invocation.WithArgument("data", data),
}, opts...)
return invocation.New(issuer, VaultWrite, subject, proofs, allOpts...)
}
// VaultSignInvocation creates an invocation to sign with a key in a vault.
func VaultSignInvocation(issuer, subject did.DID, proofs []cid.Cid, vaultCID string, keyID string, payload []byte, opts ...invocation.Option) (*invocation.Token, error) {
allOpts := append([]invocation.Option{
invocation.WithArgument("vault", vaultCID),
invocation.WithArgument("key_id", keyID),
invocation.WithArgument("payload", payload),
}, opts...)
return invocation.New(issuer, VaultSign, subject, proofs, allOpts...)
}
// DIDUpdateInvocation creates an invocation to update a DID document.
func DIDUpdateInvocation(issuer, subject did.DID, proofs []cid.Cid, targetDID string, updates map[string]any, opts ...invocation.Option) (*invocation.Token, error) {
allOpts := append([]invocation.Option{
invocation.WithArgument("did", targetDID),
invocation.WithArgument("updates", updates),
}, opts...)
return invocation.New(issuer, DIDUpdate, subject, proofs, allOpts...)
}
// DWNRecordsWriteInvocation creates an invocation to write a DWN record.
func DWNRecordsWriteInvocation(issuer, subject did.DID, proofs []cid.Cid, recordType string, data any, opts ...invocation.Option) (*invocation.Token, error) {
allOpts := append([]invocation.Option{
invocation.WithArgument("record_type", recordType),
invocation.WithArgument("data", data),
}, opts...)
return invocation.New(issuer, DWNRecordsWrite, subject, proofs, allOpts...)
}
// DWNRecordsReadInvocation creates an invocation to read DWN records.
func DWNRecordsReadInvocation(issuer, subject did.DID, proofs []cid.Cid, recordType string, filter map[string]any, opts ...invocation.Option) (*invocation.Token, error) {
allOpts := []invocation.Option{
invocation.WithArgument("record_type", recordType),
}
if filter != nil {
allOpts = append(allOpts, invocation.WithArgument("filter", filter))
}
allOpts = append(allOpts, opts...)
return invocation.New(issuer, DWNRecordsRead, subject, proofs, allOpts...)
}
// RevocationInvocation creates an invocation to revoke a delegation.
func RevocationInvocation(issuer, subject did.DID, proofs []cid.Cid, delegationCID cid.Cid, opts ...invocation.Option) (*invocation.Token, error) {
allOpts := append([]invocation.Option{
invocation.WithArgument("ucan", delegationCID),
}, opts...)
return invocation.New(issuer, UCANRevoke, subject, proofs, allOpts...)
}

View File

@@ -0,0 +1,213 @@
package ucan
import (
"github.com/ipld/go-ipld-prime"
"github.com/ucan-wg/go-ucan/pkg/policy"
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
)
// PolicyBuilder provides a fluent API for constructing UCAN policies.
// Policies are arrays of statements that form an implicit AND.
type PolicyBuilder struct {
constructors []policy.Constructor
}
// NewPolicy creates a new PolicyBuilder.
func NewPolicy() *PolicyBuilder {
return &PolicyBuilder{
constructors: make([]policy.Constructor, 0),
}
}
// Build constructs the final Policy from all added statements.
func (b *PolicyBuilder) Build() (Policy, error) {
return policy.Construct(b.constructors...)
}
// MustBuild constructs the final Policy, panicking on error.
func (b *PolicyBuilder) MustBuild() Policy {
return policy.MustConstruct(b.constructors...)
}
// Equal adds an equality constraint: selector == value
func (b *PolicyBuilder) Equal(selector string, value any) *PolicyBuilder {
node, err := toIPLDNode(value)
if err != nil {
// Store error for Build() to return
b.constructors = append(b.constructors, func() (policy.Statement, error) {
return nil, err
})
return b
}
b.constructors = append(b.constructors, policy.Equal(selector, node))
return b
}
// NotEqual adds an inequality constraint: selector != value
func (b *PolicyBuilder) NotEqual(selector string, value any) *PolicyBuilder {
node, err := toIPLDNode(value)
if err != nil {
b.constructors = append(b.constructors, func() (policy.Statement, error) {
return nil, err
})
return b
}
b.constructors = append(b.constructors, policy.NotEqual(selector, node))
return b
}
// GreaterThan adds a comparison constraint: selector > value
func (b *PolicyBuilder) GreaterThan(selector string, value int64) *PolicyBuilder {
b.constructors = append(b.constructors, policy.GreaterThan(selector, literal.Int(value)))
return b
}
// GreaterThanOrEqual adds a comparison constraint: selector >= value
func (b *PolicyBuilder) GreaterThanOrEqual(selector string, value int64) *PolicyBuilder {
b.constructors = append(b.constructors, policy.GreaterThanOrEqual(selector, literal.Int(value)))
return b
}
// LessThan adds a comparison constraint: selector < value
func (b *PolicyBuilder) LessThan(selector string, value int64) *PolicyBuilder {
b.constructors = append(b.constructors, policy.LessThan(selector, literal.Int(value)))
return b
}
// LessThanOrEqual adds a comparison constraint: selector <= value
func (b *PolicyBuilder) LessThanOrEqual(selector string, value int64) *PolicyBuilder {
b.constructors = append(b.constructors, policy.LessThanOrEqual(selector, literal.Int(value)))
return b
}
// Like adds a glob pattern constraint: selector matches pattern
// Use * for wildcard, \* for literal asterisk.
func (b *PolicyBuilder) Like(selector, pattern string) *PolicyBuilder {
b.constructors = append(b.constructors, policy.Like(selector, pattern))
return b
}
// Not negates a statement
func (b *PolicyBuilder) Not(stmt policy.Constructor) *PolicyBuilder {
b.constructors = append(b.constructors, policy.Not(stmt))
return b
}
// And adds a logical AND of multiple statements
func (b *PolicyBuilder) And(stmts ...policy.Constructor) *PolicyBuilder {
b.constructors = append(b.constructors, policy.And(stmts...))
return b
}
// Or adds a logical OR of multiple statements
func (b *PolicyBuilder) Or(stmts ...policy.Constructor) *PolicyBuilder {
b.constructors = append(b.constructors, policy.Or(stmts...))
return b
}
// All adds a universal quantifier: all elements at selector must satisfy statement
func (b *PolicyBuilder) All(selector string, stmt policy.Constructor) *PolicyBuilder {
b.constructors = append(b.constructors, policy.All(selector, stmt))
return b
}
// Any adds an existential quantifier: at least one element at selector must satisfy statement
func (b *PolicyBuilder) Any(selector string, stmt policy.Constructor) *PolicyBuilder {
b.constructors = append(b.constructors, policy.Any(selector, stmt))
return b
}
// toIPLDNode converts a Go value to an IPLD node for policy evaluation.
func toIPLDNode(value any) (ipld.Node, error) {
// Handle IPLD nodes directly
if node, ok := value.(ipld.Node); ok {
return node, nil
}
// Use literal package for conversion
return literal.Any(value)
}
// --- Sonr-specific Policy Helpers ---
// VaultPolicy creates a policy that restricts operations to a specific vault.
// The vault is identified by its CID.
func VaultPolicy(vaultCID string) Policy {
return NewPolicy().Equal(".vault", vaultCID).MustBuild()
}
// DIDPolicy creates a policy that restricts operations to DIDs matching a pattern.
// Use glob patterns: "did:sonr:*" matches all Sonr DIDs.
func DIDPolicy(didPattern string) Policy {
return NewPolicy().Like(".did", didPattern).MustBuild()
}
// ChainPolicy creates a policy that restricts operations to a specific chain.
func ChainPolicy(chainID string) Policy {
return NewPolicy().Equal(".chain_id", chainID).MustBuild()
}
// AccountPolicy creates a policy that restricts operations to a specific account address.
func AccountPolicy(address string) Policy {
return NewPolicy().Equal(".address", address).MustBuild()
}
// RecordTypePolicy creates a policy for DWN operations on specific record types.
func RecordTypePolicy(recordType string) Policy {
return NewPolicy().Equal(".record_type", recordType).MustBuild()
}
// CombinePolicies merges multiple policies into one (implicit AND).
func CombinePolicies(policies ...Policy) Policy {
combined := make(Policy, 0)
for _, p := range policies {
combined = append(combined, p...)
}
return combined
}
// EmptyPolicy returns an empty policy (no constraints).
func EmptyPolicy() Policy {
return Policy{}
}
// --- Policy Constructor Helpers ---
// These return policy.Constructor for use with And/Or/Not/All/Any
// EqualTo creates an equality constructor for nested policy building.
func EqualTo(selector string, value any) policy.Constructor {
return func() (policy.Statement, error) {
node, err := toIPLDNode(value)
if err != nil {
return nil, err
}
return policy.Equal(selector, node)()
}
}
// NotEqualTo creates an inequality constructor for nested policy building.
func NotEqualTo(selector string, value any) policy.Constructor {
return func() (policy.Statement, error) {
node, err := toIPLDNode(value)
if err != nil {
return nil, err
}
return policy.NotEqual(selector, node)()
}
}
// Matches creates a glob pattern constructor for nested policy building.
func Matches(selector, pattern string) policy.Constructor {
return policy.Like(selector, pattern)
}
// GreaterThanValue creates a comparison constructor.
func GreaterThanValue(selector string, value int64) policy.Constructor {
return policy.GreaterThan(selector, literal.Int(value))
}
// LessThanValue creates a comparison constructor.
func LessThanValue(selector string, value int64) policy.Constructor {
return policy.LessThan(selector, literal.Int(value))
}

View File

@@ -0,0 +1,261 @@
package ucan
import (
"github.com/ipfs/go-cid"
"github.com/ucan-wg/go-ucan/pkg/policy"
)
// ValidationErrorCode represents UCAN validation error types.
// These codes match the TypeScript ValidationErrorCode type in src/ucan.ts.
type ValidationErrorCode string
const (
ErrCodeExpired ValidationErrorCode = "EXPIRED"
ErrCodeNotYetValid ValidationErrorCode = "NOT_YET_VALID"
ErrCodeInvalidSignature ValidationErrorCode = "INVALID_SIGNATURE"
ErrCodePrincipalMisaligned ValidationErrorCode = "PRINCIPAL_MISALIGNMENT"
ErrCodePolicyViolation ValidationErrorCode = "POLICY_VIOLATION"
ErrCodeRevoked ValidationErrorCode = "REVOKED"
ErrCodeInvalidProofChain ValidationErrorCode = "INVALID_PROOF_CHAIN"
ErrCodeUnknownCommand ValidationErrorCode = "UNKNOWN_COMMAND"
ErrCodeMalformedToken ValidationErrorCode = "MALFORMED_TOKEN"
)
// ValidationError represents a UCAN validation failure.
type ValidationError struct {
Code ValidationErrorCode `json:"code"`
Message string `json:"message"`
Details map[string]any `json:"details,omitempty"`
}
func (e *ValidationError) Error() string {
return e.Message
}
// NewValidationError creates a validation error with the given code and message.
func NewValidationError(code ValidationErrorCode, message string) *ValidationError {
return &ValidationError{
Code: code,
Message: message,
}
}
// NewValidationErrorWithDetails creates a validation error with additional details.
func NewValidationErrorWithDetails(code ValidationErrorCode, message string, details map[string]any) *ValidationError {
return &ValidationError{
Code: code,
Message: message,
Details: details,
}
}
// Capability represents the semantically-relevant claim of a delegation.
// It combines subject, command, and policy into a single authorization unit.
type Capability struct {
// Subject DID (resource owner) - nil for powerline delegations
Subject string `json:"sub"`
// Command being delegated
Command string `json:"cmd"`
// Policy constraints on invocation arguments
Policy policy.Policy `json:"pol"`
}
// ValidationResult represents the outcome of UCAN validation.
type ValidationResult struct {
Valid bool `json:"valid"`
Capability *Capability `json:"capability,omitempty"`
Error *ValidationError `json:"error,omitempty"`
}
// ValidationSuccess creates a successful validation result.
func ValidationSuccess(cap *Capability) *ValidationResult {
return &ValidationResult{
Valid: true,
Capability: cap,
}
}
// ValidationFailure creates a failed validation result.
func ValidationFailure(err *ValidationError) *ValidationResult {
return &ValidationResult{
Valid: false,
Error: err,
}
}
// CryptoAlgorithm represents supported signature algorithms.
type CryptoAlgorithm string
const (
AlgorithmEd25519 CryptoAlgorithm = "Ed25519"
AlgorithmP256 CryptoAlgorithm = "P-256"
AlgorithmSecp256k1 CryptoAlgorithm = "secp256k1"
)
// ExecutionResult represents the outcome of an invocation execution.
type ExecutionResult[T any, E any] struct {
Ok *T `json:"ok,omitempty"`
Err *E `json:"err,omitempty"`
}
// IsSuccess returns true if the result is successful.
func (r *ExecutionResult[T, E]) IsSuccess() bool {
return r.Ok != nil
}
// IsError returns true if the result is an error.
func (r *ExecutionResult[T, E]) IsError() bool {
return r.Err != nil
}
// Success creates a successful execution result.
func Success[T any, E any](value T) *ExecutionResult[T, E] {
return &ExecutionResult[T, E]{Ok: &value}
}
// Failure creates a failed execution result.
func Failure[T any, E any](err E) *ExecutionResult[T, E] {
return &ExecutionResult[T, E]{Err: &err}
}
// ReceiptPayload represents the result of an invocation execution.
// This matches the TypeScript ReceiptPayload in src/ucan.ts.
type ReceiptPayload struct {
// Executor DID
Issuer string `json:"iss"`
// CID of executed invocation
Ran cid.Cid `json:"ran"`
// Execution result
Out *ExecutionResult[any, any] `json:"out"`
// Effects - CIDs of Tasks to enqueue
Effects []cid.Cid `json:"fx,omitempty"`
// Optional metadata
Meta map[string]any `json:"meta,omitempty"`
// Issuance timestamp (Unix seconds)
IssuedAt *int64 `json:"iat,omitempty"`
}
// RevocationPayload represents a UCAN revocation request.
// This matches the TypeScript RevocationPayload in src/ucan.ts.
type RevocationPayload struct {
// Revoker DID - must be issuer in delegation chain
Issuer string `json:"iss"`
// Subject of delegation being revoked
Subject string `json:"sub"`
// Revocation command (always "/ucan/revoke")
Command string `json:"cmd"`
// Revocation arguments
Args RevocationArgs `json:"args"`
// Proof chain
Proof []cid.Cid `json:"prf"`
// Nonce
Nonce []byte `json:"nonce"`
// Expiration (Unix seconds or null)
Expiration *int64 `json:"exp"`
}
// RevocationArgs contains the arguments for a revocation invocation.
type RevocationArgs struct {
// CID of delegation to revoke
UCAN cid.Cid `json:"ucan"`
}
// Task represents a subset of Invocation fields that uniquely determine work.
// The Task ID is the CID of these fields.
type Task struct {
// Subject DID
Subject string `json:"sub"`
// Command to execute
Command string `json:"cmd"`
// Command arguments
Args map[string]any `json:"args"`
// Nonce for uniqueness
Nonce []byte `json:"nonce"`
}
// --- Sonr-specific Types ---
// VaultCapability represents authorization for vault operations.
type VaultCapability struct {
// VaultCID identifies the specific vault
VaultCID string `json:"vault_cid"`
// Operations allowed (read, write, sign, export, import, delete, admin)
Operations []string `json:"operations"`
}
// DIDCapability represents authorization for DID operations.
type DIDCapability struct {
// DIDPattern is a glob pattern matching allowed DIDs
DIDPattern string `json:"did_pattern"`
// Operations allowed (create, update, deactivate)
Operations []string `json:"operations"`
}
// DWNCapability represents authorization for DWN operations.
type DWNCapability struct {
// RecordType specifies the allowed record type
RecordType string `json:"record_type"`
// Operations allowed (read, write, delete)
Operations []string `json:"operations"`
}
// AccountCapability represents authorization for account operations.
type AccountCapability struct {
// ChainID specifies the blockchain
ChainID string `json:"chain_id"`
// Address specifies the account (or "*" for all)
Address string `json:"address"`
// Operations allowed
Operations []string `json:"operations"`
}
// SealedToken represents a signed UCAN token with its CID and raw bytes.
type SealedToken struct {
// CID is the content identifier of the sealed token
CID cid.Cid `json:"cid"`
// Data is the DAG-CBOR encoded envelope
Data []byte `json:"data"`
// Type indicates if this is a delegation or invocation
Type string `json:"type"` // "delegation" or "invocation"
}
// SealedDelegation is a type alias for a sealed delegation token.
type SealedDelegation = SealedToken
// SealedInvocation is a type alias for a sealed invocation token.
type SealedInvocation = SealedToken
// ProofChain represents an ordered list of delegation CIDs.
// Ordered from leaf (matching invocation issuer) to root delegation.
type ProofChain []cid.Cid
// NewProofChain creates a new proof chain from CIDs.
func NewProofChain(cids ...cid.Cid) ProofChain {
return ProofChain(cids)
}
// Add appends a CID to the proof chain.
func (p *ProofChain) Add(c cid.Cid) {
*p = append(*p, c)
}
// IsEmpty returns true if the proof chain is empty.
func (p ProofChain) IsEmpty() bool {
return len(p) == 0
}
// Root returns the root delegation CID (last in chain).
func (p ProofChain) Root() (cid.Cid, bool) {
if len(p) == 0 {
return cid.Cid{}, false
}
return p[len(p)-1], true
}
// Leaf returns the leaf delegation CID (first in chain).
func (p ProofChain) Leaf() (cid.Cid, bool) {
if len(p) == 0 {
return cid.Cid{}, false
}
return p[0], true
}

View File

@@ -0,0 +1,195 @@
// Package ucan provides UCAN v1.0.0-rc.1 compliant authorization
// for the Sonr network using the official go-ucan library.
//
// This package wraps github.com/ucan-wg/go-ucan to provide:
// - Delegation creation and validation
// - Invocation creation and validation
// - Policy evaluation
// - Sonr-specific capability types (vault, did, dwn)
//
// UCAN Envelope Format (DAG-CBOR):
//
// [
// Signature, // Varsig-encoded signature
// {
// "h": VarsigHeader, // Algorithm metadata
// "ucan/dlg@1.0.0-rc.1": DelegationPayload // or "ucan/inv@1.0.0-rc.1"
// }
// ]
package ucan
import (
"github.com/ucan-wg/go-ucan/pkg/command"
"github.com/ucan-wg/go-ucan/pkg/policy"
"github.com/ucan-wg/go-ucan/token/delegation"
"github.com/ucan-wg/go-ucan/token/invocation"
)
// Re-export key types from go-ucan for convenience.
// Users should import this package instead of go-ucan directly
// for Sonr-specific functionality.
type (
// Delegation is an immutable UCAN delegation token.
Delegation = delegation.Token
// Invocation is an immutable UCAN invocation token.
Invocation = invocation.Token
// Command is a validated UCAN command string (e.g., "/vault/read").
Command = command.Command
// Policy is a list of policy statements that constrain invocation arguments.
Policy = policy.Policy
// Statement is a single policy statement (equality, like, and, or, etc.).
Statement = policy.Statement
// DelegationOption configures optional fields when creating a delegation.
DelegationOption = delegation.Option
// InvocationOption configures optional fields when creating an invocation.
InvocationOption = invocation.Option
)
// Re-export constructors
var (
// NewDelegation creates a delegation: "(issuer) allows (audience) to perform (cmd+pol) on (subject)".
NewDelegation = delegation.New
// NewRootDelegation creates a root delegation where subject == issuer.
NewRootDelegation = delegation.Root
// NewPowerlineDelegation creates a powerline delegation (subject = nil).
// Powerline automatically delegates all future delegations regardless of subject.
NewPowerlineDelegation = delegation.Powerline
// NewInvocation creates an invocation: "(issuer) executes (command) on (subject)".
NewInvocation = invocation.New
// ParseCommand validates and parses a command string.
ParseCommand = command.Parse
// MustParseCommand parses a command string, panicking on error.
MustParseCommand = command.MustParse
// TopCommand returns "/" - the most powerful capability (grants everything).
TopCommand = command.Top
// NewCommand creates a command from segments (e.g., NewCommand("vault", "read") -> "/vault/read").
NewCommand = command.New
)
// Re-export delegation options
var (
// WithExpiration sets the delegation's expiration time.
WithExpiration = delegation.WithExpiration
// WithExpirationIn sets expiration to now + duration.
WithExpirationIn = delegation.WithExpirationIn
// WithNotBefore sets when the delegation becomes valid.
WithNotBefore = delegation.WithNotBefore
// WithNotBeforeIn sets not-before to now + duration.
WithNotBeforeIn = delegation.WithNotBeforeIn
// WithDelegationMeta adds metadata to the delegation.
WithDelegationMeta = delegation.WithMeta
// WithDelegationNonce sets a custom nonce (default: random 12 bytes).
WithDelegationNonce = delegation.WithNonce
)
// Re-export invocation options
var (
// WithArgument adds a single argument to the invocation.
WithArgument = invocation.WithArgument
// WithAudience sets the invocation's audience (executor if different from subject).
WithAudience = invocation.WithAudience
// WithInvocationMeta adds metadata to the invocation.
WithInvocationMeta = invocation.WithMeta
// WithInvocationNonce sets a custom nonce.
WithInvocationNonce = invocation.WithNonce
// WithEmptyNonce sets an empty nonce for idempotent operations.
WithEmptyNonce = invocation.WithEmptyNonce
// WithInvocationExpiration sets the invocation's expiration time.
WithInvocationExpiration = invocation.WithExpiration
// WithInvocationExpirationIn sets expiration to now + duration.
WithInvocationExpirationIn = invocation.WithExpirationIn
// WithIssuedAt sets when the invocation was created.
WithIssuedAt = invocation.WithIssuedAt
// WithCause sets the receipt CID that enqueued this task.
WithCause = invocation.WithCause
)
// Standard Sonr commands following UCAN v1.0.0-rc.1 command format.
// Commands must be lowercase, start with '/', and have no trailing slash.
const (
// Vault commands
CmdVaultRead = "/vault/read"
CmdVaultWrite = "/vault/write"
CmdVaultSign = "/vault/sign"
CmdVaultExport = "/vault/export"
CmdVaultImport = "/vault/import"
CmdVaultDelete = "/vault/delete"
CmdVaultAdmin = "/vault/admin"
CmdVault = "/vault" // Superuser - grants all vault commands
// DID commands
CmdDIDCreate = "/did/create"
CmdDIDUpdate = "/did/update"
CmdDIDDeactivate = "/did/deactivate"
CmdDID = "/did" // Superuser - grants all DID commands
// DWN commands
CmdDWNRecordsWrite = "/dwn/records/write"
CmdDWNRecordsRead = "/dwn/records/read"
CmdDWNRecordsDelete = "/dwn/records/delete"
CmdDWN = "/dwn" // Superuser - grants all DWN commands
// UCAN meta commands
CmdUCANRevoke = "/ucan/revoke"
// Root command - grants everything
CmdRoot = "/"
)
// Pre-parsed Sonr commands for convenience
var (
VaultRead = command.MustParse(CmdVaultRead)
VaultWrite = command.MustParse(CmdVaultWrite)
VaultSign = command.MustParse(CmdVaultSign)
VaultExport = command.MustParse(CmdVaultExport)
VaultImport = command.MustParse(CmdVaultImport)
VaultDelete = command.MustParse(CmdVaultDelete)
VaultAdmin = command.MustParse(CmdVaultAdmin)
Vault = command.MustParse(CmdVault)
DIDCreate = command.MustParse(CmdDIDCreate)
DIDUpdate = command.MustParse(CmdDIDUpdate)
DIDDeactivate = command.MustParse(CmdDIDDeactivate)
DID = command.MustParse(CmdDID)
DWNRecordsWrite = command.MustParse(CmdDWNRecordsWrite)
DWNRecordsRead = command.MustParse(CmdDWNRecordsRead)
DWNRecordsDelete = command.MustParse(CmdDWNRecordsDelete)
DWN = command.MustParse(CmdDWN)
UCANRevoke = command.MustParse(CmdUCANRevoke)
Root = command.Top()
)
// CommandSubsumes checks if parent command subsumes child command.
// A command subsumes another if the child is a path extension of parent.
// Example: "/vault" subsumes "/vault/read" and "/vault/write"
func CommandSubsumes(parent, child Command) bool {
return parent.Covers(child)
}