Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/sequence/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ func handleEncodeSessionCallSignatures(p *EncodeSessionCallSignaturesParams) (st
ApplicationData: applicationData,
AuthData: v3.AuthData{
RedirectUrl: sig.Attestation.AuthData.RedirectUrl,
IssuedAt: sig.Attestation.AuthData.IssuedAt,
},
}

Expand Down
1 change: 1 addition & 0 deletions cmd/sequence/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ type CallSignaturesParams struct {
ApplicationData string `json:"applicationData"`
AuthData struct {
RedirectUrl string `json:"redirectUrl"`
IssuedAt uint64 `json:"issuedAt,string"`
} `json:"authData"`
} `json:"attestation"`
IdentitySignature string `json:"identitySignature"` // RSV string
Expand Down
19 changes: 19 additions & 0 deletions core/v3/attestation.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package v3

import (
"encoding/binary"
"encoding/hex"
"encoding/json"
"fmt"
Expand All @@ -21,6 +22,7 @@ type Attestation struct {
// AuthData represents authentication data with a redirect URL
type AuthData struct {
RedirectUrl string `json:"redirectUrl"`
IssuedAt uint64 `json:"issuedAt,string"`
}

// Encode converts an Attestation to its binary representation
Expand Down Expand Up @@ -55,9 +57,12 @@ func (a *Attestation) Encode() []byte {
// encodeAuthData converts AuthData to its binary representation
func encodeAuthData(authData AuthData) []byte {
redirectUrlBytes := []byte(authData.RedirectUrl)
issuedAt := make([]byte, 8)
binary.BigEndian.PutUint64(issuedAt, authData.IssuedAt)
return Concat([][]byte{
intToBytes(len(redirectUrlBytes), 3), // 3 bytes for length
redirectUrlBytes, // variable length
issuedAt, // uint64 (8 bytes)
})
}

Expand Down Expand Up @@ -149,6 +154,19 @@ func AttestationFromParsed(parsed map[string]interface{}) (*Attestation, error)
return nil, fmt.Errorf("invalid redirectUrl")
}

// issuedAt is optional.
var issuedAt uint64
if raw, ok := authData["issuedAt"]; ok && raw != nil {
n, err := bigIntFromJSON(raw)
if err != nil {
return nil, fmt.Errorf("invalid issuedAt: %w", err)
}
if n.Sign() < 0 || n.BitLen() > 64 {
return nil, fmt.Errorf("issuedAt %v out of range for uint64", n)
}
issuedAt = n.Uint64()
}

return &Attestation{
ApprovedSigner: common.HexToAddress(approvedSigner),
IdentityType: identityTypeBytes,
Expand All @@ -157,6 +175,7 @@ func AttestationFromParsed(parsed map[string]interface{}) (*Attestation, error)
ApplicationData: applicationDataBytes,
AuthData: AuthData{
RedirectUrl: redirectUrl,
IssuedAt: issuedAt,
},
}, nil
}
Expand Down
104 changes: 94 additions & 10 deletions core/v3/permission.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
"math"
"math/big"
"strings"

Expand Down Expand Up @@ -39,6 +40,7 @@ type Permission struct {
// SessionPermissions groups a signer with its associated permissions.
type SessionPermissions struct {
Signer common.Address `json:"signer"`
ChainID *big.Int `json:"chainId"`
ValueLimit *big.Int `json:"valueLimit"`
Deadline *big.Int `json:"deadline"`
Permissions []Permission `json:"permissions"`
Expand All @@ -61,9 +63,35 @@ func EncodeSessionPermissions(sp *SessionPermissions) ([]byte, error) {
var result []byte
// Append signer as 20-byte left‐padded value.
result = append(result, LeftPad(sp.Signer.Bytes(), 20)...)
// Append valueLimit (32 bytes) and deadline (32 bytes).
result = append(result, LeftPad(sp.ValueLimit.Bytes(), 32)...)
result = append(result, LeftPad(sp.Deadline.Bytes(), 32)...)
// Append chainId (32 bytes). 0 means any chain.
chainID := sp.ChainID
if chainID == nil {
chainID = big.NewInt(0)
}
if chainID.Sign() < 0 || chainID.BitLen() > 256 {
return nil, fmt.Errorf("chainId %v out of range for uint256", chainID)
}
result = append(result, LeftPad(chainID.Bytes(), 32)...)
Comment thread
ScreamingHawk marked this conversation as resolved.
// Append valueLimit (32 bytes).
valueLimit := sp.ValueLimit
if valueLimit == nil {
valueLimit = big.NewInt(0)
}
if valueLimit.Sign() < 0 || valueLimit.BitLen() > 256 {
return nil, fmt.Errorf("valueLimit %v out of range for uint256", valueLimit)
}
result = append(result, LeftPad(valueLimit.Bytes(), 32)...)
// Append deadline (uint64, 8 bytes).
deadline := sp.Deadline
if deadline == nil {
deadline = big.NewInt(0)
}
if deadline.Sign() < 0 || deadline.BitLen() > 64 {
return nil, fmt.Errorf("deadline %v out of range for uint64", deadline)
}
var deadlineBuf [8]byte
deadline.FillBytes(deadlineBuf[:])
Comment thread
ScreamingHawk marked this conversation as resolved.
result = append(result, deadlineBuf[:]...)
// Append a single byte with the number of permissions.
result = append(result, byte(len(sp.Permissions)))
// Encode each permission.
Expand Down Expand Up @@ -115,15 +143,16 @@ func boolToByte(b bool) byte {

// DecodeSessionPermissions decodes a byte slice into a SessionPermissions structure.
func DecodeSessionPermissions(b []byte) (SessionPermissions, error) {
if len(b) < 85 {
if len(b) < 93 {
return SessionPermissions{}, fmt.Errorf("insufficient bytes for session permissions")
}
var sp SessionPermissions
sp.Signer = common.BytesToAddress(b[0:20])
sp.ValueLimit = new(big.Int).SetBytes(b[20:52])
sp.Deadline = new(big.Int).SetBytes(b[52:84])
permCount := int(b[84])
ptr := 85
sp.ChainID = new(big.Int).SetBytes(b[20:52])
sp.ValueLimit = new(big.Int).SetBytes(b[52:84])
sp.Deadline = new(big.Int).SetBytes(b[84:92])
permCount := int(b[92])
ptr := 93
var perms []Permission
for i := 0; i < permCount; i++ {
perm, consumed, err := decodePermission(b[ptr:])
Expand Down Expand Up @@ -275,8 +304,13 @@ func encodeSessionPermissionsForJson(sp *SessionPermissions) map[string]interfac
for _, p := range sp.Permissions {
perms = append(perms, encodePermissionForJson(&p))
}
chainID := sp.ChainID
if chainID == nil {
chainID = big.NewInt(0)
}
return map[string]interface{}{
"signer": sp.Signer.Hex(),
"chainId": chainID.String(),
"valueLimit": sp.ValueLimit.String(),
"deadline": sp.Deadline.String(),
"permissions": perms,
Expand Down Expand Up @@ -355,10 +389,31 @@ func sessionPermissionsFromParsed(parsed interface{}) (SessionPermissions, error
perms[i] = perm
}

// chainId is optional; a missing value means any chain (0).
chainID := big.NewInt(0)
if raw, ok := m["chainId"]; ok && raw != nil {
var err error
chainID, err = bigIntFromJSON(raw)
if err != nil {
return SessionPermissions{}, fmt.Errorf("invalid chainId: %w", err)
}
}

valueLimit, err := bigIntFromJSON(m["valueLimit"])
if err != nil {
return SessionPermissions{}, fmt.Errorf("invalid valueLimit: %w", err)
}

deadline, err := bigIntFromJSON(m["deadline"])
if err != nil {
return SessionPermissions{}, fmt.Errorf("invalid deadline: %w", err)
}

return SessionPermissions{
Signer: common.HexToAddress(m["signer"].(string)),
ValueLimit: valueToBigInt(m["valueLimit"]),
Deadline: valueToBigInt(m["deadline"]),
ChainID: chainID,
ValueLimit: valueLimit,
Deadline: deadline,
Permissions: perms,
}, nil
}
Expand Down Expand Up @@ -447,6 +502,35 @@ func mustDecodeHex(s string) []byte {
return b
}

// bigIntFromJSON converts a JSON value (a decimal string or number) to a
// big.Int. Numbers are accepted only as exact integers within float64's safe
// range; larger values must be sent as strings, because encoding/json decodes
// an unquoted number into a float64 and silently rounds anything at or above
// 2^53 before it reaches here.
func bigIntFromJSON(v interface{}) (*big.Int, error) {
switch val := v.(type) {
case string:
i, ok := new(big.Int).SetString(val, 10)
if !ok {
return nil, fmt.Errorf("invalid numeric string %q", val)
}
return i, nil
case json.Number:
i, ok := new(big.Int).SetString(val.String(), 10)
if !ok {
return nil, fmt.Errorf("invalid number %q", val.String())
}
return i, nil
case float64:
if val != math.Trunc(val) || math.Abs(val) >= 1<<53 {
return nil, fmt.Errorf("numeric value %v must be an integer within 2^53 or sent as a string", val)
}
return big.NewInt(int64(val)), nil
default:
return nil, fmt.Errorf("invalid numeric type %T", v)
}
}

// valueToBigInt converts a string (or number) to *big.Int.
func valueToBigInt(v interface{}) *big.Int {
switch val := v.(type) {
Expand Down
6 changes: 6 additions & 0 deletions core/v3/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -1017,6 +1017,12 @@ func HashPayloadCallIdx(payload CallsPayload, callIdx int) (common.Hash, error)
if callIdx < 0 || callIdx >= len(payload.Calls) {
return common.Hash{}, fmt.Errorf("call index %v out of range [0, %v)", callIdx, len(payload.Calls))
}
// The session-holding wallet is the last parent wallet in the signing
// context, but on chain it is the verifying contract (msg.sender), not a
// parent entry. Drop it before hashing to match SessionSig.hashPayloadCallIdx.
if n := len(payload.parentWallets); n > 0 {
payload.parentWallets = payload.parentWallets[:n-1]
Comment thread
ScreamingHawk marked this conversation as resolved.
}
payloadHash := payload.Digest().Hash
var idx [32]byte
big.NewInt(int64(callIdx)).FillBytes(idx[:])
Expand Down
33 changes: 33 additions & 0 deletions core/v3/session_digest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,36 @@ func TestHashPayloadCallIdx(t *testing.T) {
t.Errorf("expected out-of-range error for call index -1")
}
}

// TestHashPayloadCallIdxParentWallets checks that the session wallet,
// which is the last parent wallet in the signing context, is dropped before
// hashing (on chain it is the verifying contract, not a parent). The expected
// digest was produced by SessionSig.hashPayloadCallIdx for the same payload
// carrying only the real parent wallet.
func TestHashPayloadCallIdxParentWallets(t *testing.T) {
wallet := common.HexToAddress("0x1111111111111111111111111111111111111111")
parent := common.HexToAddress("0x9999999999999999999999999999999999999999")

payload := v3.NewCallsPayload(
wallet,
big.NewInt(42161),
[]v3.Call{
{
To: common.HexToAddress("0x2222222222222222222222222222222222222222"),
BehaviorOnError: v3.BehaviorOnErrorRevert,
},
},
big.NewInt(0),
big.NewInt(7),
[]common.Address{parent, wallet},
)

expected := common.HexToHash("0x29f4f05700f22db9cfe0272b824961355b2383109f0309542b9a5c495103b69f")
hash, err := v3.HashPayloadCallIdx(payload, 0)
if err != nil {
t.Fatalf("HashPayloadCallIdx: %v", err)
}
if hash != expected {
t.Errorf("digest mismatch: got %v, expected %v", hash, expected)
}
}
Loading
Loading