mirror of
https://github.com/sonr-io/crypto.git
synced 2026-08-02 15:31:38 +00:00
No commit suggestions generated
This commit is contained in:
+116
@@ -0,0 +1,116 @@
|
||||
package spec
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/sonr-io/sonr/crypto/mpc"
|
||||
)
|
||||
|
||||
// MPCSigningMethod implements the SigningMethod interface for MPC-based signing
|
||||
type MPCSigningMethod struct {
|
||||
Name string
|
||||
enclave mpc.Enclave
|
||||
}
|
||||
|
||||
// NewJWTSigningMethod creates a new MPC signing method with the given enclave
|
||||
func NewJWTSigningMethod(name string, enclave mpc.Enclave) *MPCSigningMethod {
|
||||
return &MPCSigningMethod{
|
||||
Name: name,
|
||||
enclave: enclave,
|
||||
}
|
||||
}
|
||||
|
||||
// WithEnclave sets the enclave for an existing signing method
|
||||
func (m *MPCSigningMethod) WithEnclave(enclave mpc.Enclave) *MPCSigningMethod {
|
||||
return &MPCSigningMethod{
|
||||
Name: m.Name,
|
||||
enclave: enclave,
|
||||
}
|
||||
}
|
||||
|
||||
// NewMPCSigningMethod is an alias for NewJWTSigningMethod for compatibility
|
||||
func NewMPCSigningMethod(name string, enclave mpc.Enclave) *MPCSigningMethod {
|
||||
return NewJWTSigningMethod(name, enclave)
|
||||
}
|
||||
|
||||
// Alg returns the signing method's name
|
||||
func (m *MPCSigningMethod) Alg() string {
|
||||
return m.Name
|
||||
}
|
||||
|
||||
// Verify verifies the signature using the MPC public key
|
||||
func (m *MPCSigningMethod) Verify(signingString string, signature []byte, key any) error {
|
||||
// Check if enclave is available
|
||||
if m.enclave == nil {
|
||||
return fmt.Errorf("MPC enclave not available for signature verification")
|
||||
}
|
||||
|
||||
// Decode the signature
|
||||
sig, err := base64.RawURLEncoding.DecodeString(string(signature))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode signature: %w", err)
|
||||
}
|
||||
|
||||
// Hash the signing string using SHA-256
|
||||
hasher := sha256.New()
|
||||
hasher.Write([]byte(signingString))
|
||||
digest := hasher.Sum(nil)
|
||||
|
||||
// Use MPC enclave to verify signature
|
||||
valid, err := m.enclave.Verify(digest, sig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to verify signature: %w", err)
|
||||
}
|
||||
|
||||
if !valid {
|
||||
return fmt.Errorf("signature verification failed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sign signs the data using MPC
|
||||
func (m *MPCSigningMethod) Sign(signingString string, key any) ([]byte, error) {
|
||||
// Check if enclave is available
|
||||
if m.enclave == nil {
|
||||
return nil, fmt.Errorf("MPC enclave not available for signing")
|
||||
}
|
||||
|
||||
// Hash the signing string using SHA-256
|
||||
hasher := sha256.New()
|
||||
hasher.Write([]byte(signingString))
|
||||
digest := hasher.Sum(nil)
|
||||
|
||||
// Use MPC enclave to sign the digest
|
||||
sig, err := m.enclave.Sign(digest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to sign with MPC: %w", err)
|
||||
}
|
||||
|
||||
// Encode the signature as base64url
|
||||
encoded := base64.RawURLEncoding.EncodeToString(sig)
|
||||
return []byte(encoded), nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Register the MPC signing method factory
|
||||
jwt.RegisterSigningMethod("MPC256", func() jwt.SigningMethod {
|
||||
// This factory creates a new instance without enclave
|
||||
// The enclave will be provided when creating tokens
|
||||
return &MPCSigningMethod{
|
||||
Name: "MPC256",
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// RegisterMPCMethod registers an MPC signing method for the given algorithm name
|
||||
func RegisterMPCMethod(alg string) {
|
||||
jwt.RegisterSigningMethod(alg, func() jwt.SigningMethod {
|
||||
return &MPCSigningMethod{
|
||||
Name: alg,
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
package spec
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/libp2p/go-libp2p/core/crypto"
|
||||
"github.com/sonr-io/sonr/crypto/keys"
|
||||
"github.com/sonr-io/sonr/crypto/mpc"
|
||||
"lukechampine.com/blake3"
|
||||
)
|
||||
|
||||
// KeyshareSource provides MPC-based UCAN token creation and validation
|
||||
type KeyshareSource interface {
|
||||
Address() string
|
||||
Issuer() string
|
||||
ChainCode() ([]byte, error)
|
||||
OriginToken() (*Token, error)
|
||||
SignData(data []byte) ([]byte, error)
|
||||
VerifyData(data []byte, sig []byte) (bool, error)
|
||||
Enclave() mpc.Enclave
|
||||
|
||||
// UCAN token creation methods
|
||||
NewOriginToken(
|
||||
audienceDID string,
|
||||
att []Attenuation,
|
||||
fct []Fact,
|
||||
notBefore, expires time.Time,
|
||||
) (*Token, error)
|
||||
NewAttenuatedToken(
|
||||
parent *Token,
|
||||
audienceDID string,
|
||||
att []Attenuation,
|
||||
fct []Fact,
|
||||
nbf, exp time.Time,
|
||||
) (*Token, error)
|
||||
}
|
||||
|
||||
// NewSource creates a new MPC-based keyshare source from an enclave
|
||||
func NewSource(enclave mpc.Enclave) (KeyshareSource, error) {
|
||||
if !enclave.IsValid() {
|
||||
return nil, fmt.Errorf("invalid MPC enclave provided")
|
||||
}
|
||||
|
||||
pubKeyBytes := enclave.PubKeyBytes()
|
||||
issuerDID, addr, err := getIssuerDIDFromBytes(pubKeyBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to derive issuer DID: %w", err)
|
||||
}
|
||||
|
||||
return &mpcKeyshareSource{
|
||||
enclave: enclave,
|
||||
issuerDID: issuerDID,
|
||||
addr: addr,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// mpcKeyshareSource implements KeyshareSource using MPC enclave
|
||||
type mpcKeyshareSource struct {
|
||||
enclave mpc.Enclave
|
||||
issuerDID string
|
||||
addr string
|
||||
}
|
||||
|
||||
// Address returns the address derived from the enclave public key
|
||||
func (k *mpcKeyshareSource) Address() string {
|
||||
return k.addr
|
||||
}
|
||||
|
||||
// Issuer returns the DID of the issuer derived from the enclave public key
|
||||
func (k *mpcKeyshareSource) Issuer() string {
|
||||
return k.issuerDID
|
||||
}
|
||||
|
||||
// Enclave returns the underlying MPC enclave
|
||||
func (k *mpcKeyshareSource) Enclave() mpc.Enclave {
|
||||
return k.enclave
|
||||
}
|
||||
|
||||
// ChainCode derives a deterministic chain code from the enclave
|
||||
func (k *mpcKeyshareSource) ChainCode() ([]byte, error) {
|
||||
// Sign the address to create a deterministic chain code
|
||||
sig, err := k.SignData([]byte(k.addr))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to sign address for chain code: %w", err)
|
||||
}
|
||||
|
||||
// Hash the signature to create a 32-byte chain code
|
||||
hash := blake3.Sum256(sig)
|
||||
return hash[:32], nil
|
||||
}
|
||||
|
||||
// OriginToken creates a default origin token with basic capabilities
|
||||
func (k *mpcKeyshareSource) OriginToken() (*Token, error) {
|
||||
// Create basic capability for the MPC keyshare
|
||||
resource := &SimpleResource{
|
||||
Scheme: "mpc",
|
||||
Value: k.addr,
|
||||
URI: fmt.Sprintf("mpc://%s", k.addr),
|
||||
}
|
||||
|
||||
capability := &SimpleCapability{Action: "sign"}
|
||||
|
||||
attenuation := Attenuation{
|
||||
Capability: capability,
|
||||
Resource: resource,
|
||||
}
|
||||
|
||||
// Create token with no expiration for origin token
|
||||
zero := time.Time{}
|
||||
return k.NewOriginToken(k.issuerDID, []Attenuation{attenuation}, nil, zero, zero)
|
||||
}
|
||||
|
||||
// SignData signs data using the MPC enclave
|
||||
func (k *mpcKeyshareSource) SignData(data []byte) ([]byte, error) {
|
||||
if !k.enclave.IsValid() {
|
||||
return nil, fmt.Errorf("enclave is not valid")
|
||||
}
|
||||
|
||||
return k.enclave.Sign(data)
|
||||
}
|
||||
|
||||
// VerifyData verifies a signature using the MPC enclave
|
||||
func (k *mpcKeyshareSource) VerifyData(data []byte, sig []byte) (bool, error) {
|
||||
if !k.enclave.IsValid() {
|
||||
return false, fmt.Errorf("enclave is not valid")
|
||||
}
|
||||
|
||||
return k.enclave.Verify(data, sig)
|
||||
}
|
||||
|
||||
// NewOriginToken creates a new UCAN origin token using MPC signing
|
||||
func (k *mpcKeyshareSource) NewOriginToken(
|
||||
audienceDID string,
|
||||
att []Attenuation,
|
||||
fct []Fact,
|
||||
notBefore, expires time.Time,
|
||||
) (*Token, error) {
|
||||
return k.newToken(audienceDID, nil, att, fct, notBefore, expires)
|
||||
}
|
||||
|
||||
// NewAttenuatedToken creates a new attenuated UCAN token using MPC signing
|
||||
func (k *mpcKeyshareSource) NewAttenuatedToken(
|
||||
parent *Token,
|
||||
audienceDID string,
|
||||
att []Attenuation,
|
||||
fct []Fact,
|
||||
nbf, exp time.Time,
|
||||
) (*Token, error) {
|
||||
// Validate that new attenuations are more restrictive than parent
|
||||
if !isAttenuationSubset(att, parent.Attenuations) {
|
||||
return nil, fmt.Errorf("scope of ucan attenuations must be less than its parent")
|
||||
}
|
||||
|
||||
// Add parent as proof
|
||||
proofs := []Proof{}
|
||||
if parent.Raw != "" {
|
||||
proofs = append(proofs, Proof(parent.Raw))
|
||||
}
|
||||
proofs = append(proofs, parent.Proofs...)
|
||||
|
||||
return k.newToken(audienceDID, proofs, att, fct, nbf, exp)
|
||||
}
|
||||
|
||||
// newToken creates a new UCAN token with MPC signing
|
||||
func (k *mpcKeyshareSource) newToken(
|
||||
audienceDID string,
|
||||
proofs []Proof,
|
||||
att []Attenuation,
|
||||
fct []Fact,
|
||||
nbf, exp time.Time,
|
||||
) (*Token, error) {
|
||||
// Validate audience DID
|
||||
if !isValidDID(audienceDID) {
|
||||
return nil, fmt.Errorf("invalid audience DID: %s", audienceDID)
|
||||
}
|
||||
|
||||
// Create JWT with MPC signing method
|
||||
t := jwt.New(NewJWTSigningMethod("MPC256", k.enclave))
|
||||
|
||||
// Set UCAN version header
|
||||
t.Header[UCANVersionKey] = UCANVersion
|
||||
|
||||
var (
|
||||
nbfUnix int64
|
||||
expUnix int64
|
||||
)
|
||||
|
||||
if !nbf.IsZero() {
|
||||
nbfUnix = nbf.Unix()
|
||||
}
|
||||
if !exp.IsZero() {
|
||||
expUnix = exp.Unix()
|
||||
}
|
||||
|
||||
// Convert attenuations to claim format
|
||||
attClaims := make([]map[string]any, len(att))
|
||||
for i, a := range att {
|
||||
attClaims[i] = map[string]any{
|
||||
"can": a.Capability.GetActions(),
|
||||
"with": a.Resource.GetURI(),
|
||||
}
|
||||
}
|
||||
|
||||
// Convert proofs to strings
|
||||
proofStrings := make([]string, len(proofs))
|
||||
for i, proof := range proofs {
|
||||
proofStrings[i] = string(proof)
|
||||
}
|
||||
|
||||
// Convert facts to any slice
|
||||
factData := make([]any, len(fct))
|
||||
for i, fact := range fct {
|
||||
factData[i] = string(fact.Data)
|
||||
}
|
||||
|
||||
// Set claims
|
||||
claims := jwt.MapClaims{
|
||||
"iss": k.issuerDID,
|
||||
"aud": audienceDID,
|
||||
"att": attClaims,
|
||||
}
|
||||
|
||||
if nbfUnix > 0 {
|
||||
claims["nbf"] = nbfUnix
|
||||
}
|
||||
if expUnix > 0 {
|
||||
claims["exp"] = expUnix
|
||||
}
|
||||
if len(proofStrings) > 0 {
|
||||
claims["prf"] = proofStrings
|
||||
}
|
||||
if len(factData) > 0 {
|
||||
claims["fct"] = factData
|
||||
}
|
||||
|
||||
t.Claims = claims
|
||||
|
||||
// Sign the token using MPC enclave
|
||||
tokenString, err := t.SignedString(nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to sign token: %w", err)
|
||||
}
|
||||
|
||||
return &Token{
|
||||
Raw: tokenString,
|
||||
Issuer: k.issuerDID,
|
||||
Audience: audienceDID,
|
||||
ExpiresAt: expUnix,
|
||||
NotBefore: nbfUnix,
|
||||
Attenuations: att,
|
||||
Proofs: proofs,
|
||||
Facts: fct,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// isAttenuationSubset checks if child attenuations are a subset of parent attenuations
|
||||
func isAttenuationSubset(child, parent []Attenuation) bool {
|
||||
for _, childAtt := range child {
|
||||
if !containsAttenuation(parent, childAtt) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// containsAttenuation checks if the parent list contains an equivalent attenuation
|
||||
func containsAttenuation(parent []Attenuation, att Attenuation) bool {
|
||||
for _, parentAtt := range parent {
|
||||
if parentAtt.Resource.Matches(att.Resource) &&
|
||||
parentAtt.Capability.Contains(att.Capability) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isValidDID validates DID format
|
||||
func isValidDID(did string) bool {
|
||||
return did != "" && len(did) > 5 && strings.HasPrefix(did, "did:")
|
||||
}
|
||||
|
||||
// getIssuerDIDFromBytes creates an issuer DID and address from public key bytes
|
||||
func getIssuerDIDFromBytes(pubKeyBytes []byte) (string, string, error) {
|
||||
// Convert MPC public key bytes to libp2p crypto.PubKey
|
||||
pubKey, err := crypto.UnmarshalSecp256k1PublicKey(pubKeyBytes)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to unmarshal secp256k1 key: %w", err)
|
||||
}
|
||||
|
||||
// Create DID using the crypto/keys package
|
||||
did, err := keys.NewDID(pubKey)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to create DID: %w", err)
|
||||
}
|
||||
|
||||
didStr := did.String()
|
||||
|
||||
// Generate address from DID (simplified implementation)
|
||||
address := fmt.Sprintf("addr_%x", pubKeyBytes[:8])
|
||||
|
||||
return didStr, address, nil
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package spec
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/types/bech32"
|
||||
)
|
||||
|
||||
// Token represents a UCAN JWT token with parsed claims
|
||||
type Token struct {
|
||||
Raw string `json:"raw"`
|
||||
Issuer string `json:"iss"`
|
||||
Audience string `json:"aud"`
|
||||
ExpiresAt int64 `json:"exp,omitempty"`
|
||||
NotBefore int64 `json:"nbf,omitempty"`
|
||||
Attenuations []Attenuation `json:"att"`
|
||||
Proofs []Proof `json:"prf,omitempty"`
|
||||
Facts []Fact `json:"fct,omitempty"`
|
||||
}
|
||||
|
||||
// Attenuation represents a UCAN capability attenuation
|
||||
type Attenuation struct {
|
||||
Capability Capability `json:"can"`
|
||||
Resource Resource `json:"with"`
|
||||
}
|
||||
|
||||
// Proof represents a UCAN delegation proof (either JWT or CID)
|
||||
type Proof string
|
||||
|
||||
// Fact represents arbitrary facts in UCAN tokens
|
||||
type Fact struct {
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
|
||||
// Capability defines what actions can be performed
|
||||
type Capability interface {
|
||||
GetActions() []string
|
||||
Grants(abilities []string) bool
|
||||
Contains(other Capability) bool
|
||||
String() string
|
||||
}
|
||||
|
||||
// Resource defines what resource the capability applies to
|
||||
type Resource interface {
|
||||
GetScheme() string
|
||||
GetValue() string
|
||||
GetURI() string
|
||||
Matches(other Resource) bool
|
||||
}
|
||||
|
||||
// SimpleCapability implements Capability for single actions
|
||||
type SimpleCapability struct {
|
||||
Action string `json:"action"`
|
||||
}
|
||||
|
||||
func (c *SimpleCapability) GetActions() []string { return []string{c.Action} }
|
||||
func (c *SimpleCapability) Grants(abilities []string) bool {
|
||||
return len(abilities) == 1 && c.Action == abilities[0]
|
||||
}
|
||||
|
||||
func (c *SimpleCapability) Contains(
|
||||
other Capability,
|
||||
) bool {
|
||||
return c.Action == other.GetActions()[0]
|
||||
}
|
||||
func (c *SimpleCapability) String() string { return c.Action }
|
||||
|
||||
// SimpleResource implements Resource for basic URI resources
|
||||
type SimpleResource struct {
|
||||
Scheme string `json:"scheme"`
|
||||
Value string `json:"value"`
|
||||
URI string `json:"uri"`
|
||||
}
|
||||
|
||||
func (r *SimpleResource) GetScheme() string { return r.Scheme }
|
||||
func (r *SimpleResource) GetValue() string { return r.Value }
|
||||
func (r *SimpleResource) GetURI() string { return r.URI }
|
||||
func (r *SimpleResource) Matches(other Resource) bool { return r.URI == other.GetURI() }
|
||||
|
||||
// UCAN constants
|
||||
const (
|
||||
UCANVersion = "0.9.0"
|
||||
UCANVersionKey = "ucv"
|
||||
PrfKey = "prf"
|
||||
FctKey = "fct"
|
||||
AttKey = "att"
|
||||
CapKey = "cap"
|
||||
)
|
||||
|
||||
// CreateSimpleAttenuation creates a basic attenuation
|
||||
func CreateSimpleAttenuation(action, resourceURI string) Attenuation {
|
||||
return Attenuation{
|
||||
Capability: &SimpleCapability{Action: action},
|
||||
Resource: parseResourceURI(resourceURI),
|
||||
}
|
||||
}
|
||||
|
||||
// parseResourceURI creates a Resource from URI string
|
||||
func parseResourceURI(uri string) Resource {
|
||||
parts := strings.SplitN(uri, "://", 2)
|
||||
if len(parts) != 2 {
|
||||
return &SimpleResource{
|
||||
Scheme: "unknown",
|
||||
Value: uri,
|
||||
URI: uri,
|
||||
}
|
||||
}
|
||||
|
||||
return &SimpleResource{
|
||||
Scheme: parts[0],
|
||||
Value: parts[1],
|
||||
URI: uri,
|
||||
}
|
||||
}
|
||||
|
||||
// getIssuerDIDFromBytes creates an issuer DID and address from public key bytes (alternative implementation)
|
||||
func getIssuerDIDFromBytesAlt(pubKeyBytes []byte) (string, string, error) {
|
||||
addr, err := bech32.ConvertAndEncode("idx", pubKeyBytes)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to encode address: %w", err)
|
||||
}
|
||||
return fmt.Sprintf("did:sonr:%s", addr), addr, nil
|
||||
}
|
||||
Reference in New Issue
Block a user