feat(ucan): add delegation and invocation builders
This commit is contained in:
271
internal/crypto/ucan/delegation.go
Normal file
271
internal/crypto/ucan/delegation.go
Normal 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...)
|
||||||
|
}
|
||||||
259
internal/crypto/ucan/invocation.go
Normal file
259
internal/crypto/ucan/invocation.go
Normal 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...)
|
||||||
|
}
|
||||||
213
internal/crypto/ucan/policy.go
Normal file
213
internal/crypto/ucan/policy.go
Normal 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))
|
||||||
|
}
|
||||||
261
internal/crypto/ucan/types.go
Normal file
261
internal/crypto/ucan/types.go
Normal 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
|
||||||
|
}
|
||||||
195
internal/crypto/ucan/ucan.go
Normal file
195
internal/crypto/ucan/ucan.go
Normal 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)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user