@agenticprimitives/contracts 0.1.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AUDIT.md +67 -0
- package/CLAUDE.md +40 -0
- package/LICENSE +21 -0
- package/README.md +45 -0
- package/deployments-anvil.json +1 -0
- package/deployments-base-sepolia.json +1 -0
- package/dist/abi/AgentNameAttributeResolver.json +798 -0
- package/dist/abi/AgentNamePredicates.json +1 -0
- package/dist/abi/AgentNameRegistry.json +826 -0
- package/dist/abi/AgentNameUniversalResolver.json +222 -0
- package/dist/abi/AgentProfilePredicates.json +1 -0
- package/dist/abi/AgentProfileResolver.json +1044 -0
- package/dist/abi/AgentRelationship.json +583 -0
- package/dist/abi/AgentRelationshipPredicates.json +1 -0
- package/dist/abi/AgenticGovernance.json +259 -0
- package/dist/abi/AllowedMethodsEnforcer.json +108 -0
- package/dist/abi/AllowedTargetsEnforcer.json +103 -0
- package/dist/abi/ApprovedHashRegistry.json +114 -0
- package/dist/abi/AttributeStorage.json +557 -0
- package/dist/abi/CaveatEnforcerBase.json +130 -0
- package/dist/abi/GovernanceManaged.json +43 -0
- package/dist/abi/IAttributeReader.json +98 -0
- package/dist/abi/ICaveatEnforcer.json +98 -0
- package/dist/abi/IDelegationManager.json +211 -0
- package/dist/abi/IERC7579Module.json +34 -0
- package/dist/abi/IERC7579ModuleLifecycle.json +60 -0
- package/dist/abi/IGovernanceView.json +34 -0
- package/dist/abi/MultiSendCallOnly.json +29 -0
- package/dist/abi/MultiSendCallOnlyHarness.json +42 -0
- package/dist/abi/OntologyTermRegistry.json +397 -0
- package/dist/abi/P256Verifier.json +1 -0
- package/dist/abi/PermissionlessSubregistry.json +207 -0
- package/dist/abi/RelationshipTypeRegistry.json +455 -0
- package/dist/abi/ShapeRegistry.json +627 -0
- package/dist/abi/SmartAgentModuleTypes.json +1 -0
- package/dist/abi/TimestampEnforcer.json +108 -0
- package/dist/abi/ValueEnforcer.json +103 -0
- package/dist/abi/WebAuthnLib.json +1 -0
- package/dist/abi/index.d.ts +35 -0
- package/dist/abi/index.js +35 -0
- package/package.json +48 -0
- package/spec.md +52 -0
- package/src/AgentAccount.sol +1374 -0
- package/src/AgentAccountFactory.sol +274 -0
- package/src/ApprovedHashRegistry.sol +57 -0
- package/src/IAgentAccount.sol +138 -0
- package/src/SmartAgentPaymaster.sol +281 -0
- package/src/UniversalSignatureValidator.sol +136 -0
- package/src/agency/DelegationManager.sol +374 -0
- package/src/agency/ICaveatEnforcer.sol +62 -0
- package/src/agency/IDelegationManager.sol +69 -0
- package/src/custody/CustodyPolicy.sol +892 -0
- package/src/custody/IERC7579Module.sol +60 -0
- package/src/enforcers/AllowedMethodsEnforcer.AUDIT.md +51 -0
- package/src/enforcers/AllowedMethodsEnforcer.sol +48 -0
- package/src/enforcers/AllowedTargetsEnforcer.AUDIT.md +49 -0
- package/src/enforcers/AllowedTargetsEnforcer.sol +44 -0
- package/src/enforcers/CaveatEnforcerBase.sol +19 -0
- package/src/enforcers/QuorumEnforcer.AUDIT.md +71 -0
- package/src/enforcers/QuorumEnforcer.sol +191 -0
- package/src/enforcers/TimestampEnforcer.AUDIT.md +50 -0
- package/src/enforcers/TimestampEnforcer.sol +43 -0
- package/src/enforcers/ValueEnforcer.AUDIT.md +51 -0
- package/src/enforcers/ValueEnforcer.sol +41 -0
- package/src/governance/AgenticGovernance.sol +140 -0
- package/src/governance/GovernanceManaged.sol +75 -0
- package/src/governance/IGovernance.sol +15 -0
- package/src/identity/AgentProfilePredicates.sol +40 -0
- package/src/identity/AgentProfileResolver.sol +194 -0
- package/src/libraries/MultiSendCallOnly.sol +95 -0
- package/src/libraries/P256Verifier.sol +47 -0
- package/src/libraries/SignatureSlotRecovery.sol +196 -0
- package/src/libraries/WebAuthnLib.sol +164 -0
- package/src/naming/AgentNameAttributeResolver.sol +95 -0
- package/src/naming/AgentNamePredicates.sol +74 -0
- package/src/naming/AgentNameRegistry.sol +362 -0
- package/src/naming/AgentNameUniversalResolver.sol +210 -0
- package/src/naming/PermissionlessSubregistry.sol +98 -0
- package/src/ontology/AttributeStorage.sol +289 -0
- package/src/ontology/OntologyTermRegistry.sol +146 -0
- package/src/ontology/ShapeRegistry.sol +240 -0
- package/src/relationships/AgentRelationship.sol +289 -0
- package/src/relationships/AgentRelationshipPredicates.sol +44 -0
- package/src/relationships/RelationshipTypeRegistry.sol +143 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.28;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @title P256Verifier
|
|
6
|
+
* @notice Dispatcher for P-256 (secp256r1) signature verification.
|
|
7
|
+
*
|
|
8
|
+
* **H7-C.2 / CON-P256-001 closure.** Previously this library
|
|
9
|
+
* silently fell through to the Daimo P256Verifier at the hardcoded
|
|
10
|
+
* address `0xc2b78104907F722DABAc4C69f826a522B2754De4` when the
|
|
11
|
+
* RIP-7212 precompile was absent. Two problems:
|
|
12
|
+
*
|
|
13
|
+
* 1. `try fast catch slow` pattern — direct ADR-0013 violation
|
|
14
|
+
* (one mechanism per security path; an attacker squatting that
|
|
15
|
+
* address on a fork made the on-chain verifier accept whatever
|
|
16
|
+
* the malicious contract returned).
|
|
17
|
+
* 2. Un-version-pinned third-party dependency — if Daimo's
|
|
18
|
+
* upgrade keys (or address) ever change, every account on
|
|
19
|
+
* every chain that fell back becomes compromised in lockstep.
|
|
20
|
+
*
|
|
21
|
+
* This library now uses **RIP-7212 only**. Chains without the
|
|
22
|
+
* precompile (e.g. pre-Pectra Ethereum mainnet) cannot use this
|
|
23
|
+
* verifier and MUST wire a separate, explicitly configured
|
|
24
|
+
* pure-Solidity P-256 verifier at the consumer layer. That config
|
|
25
|
+
* is intentionally NOT in this library — a hardcoded fallback in
|
|
26
|
+
* a security primitive is the exact pattern the audit rejected.
|
|
27
|
+
*
|
|
28
|
+
* Live deployments at the time of H7-C.2:
|
|
29
|
+
* - Base / Base Sepolia ✓ RIP-7212 native
|
|
30
|
+
* - Polygon zkEVM ✓
|
|
31
|
+
* - Optimism (post-Granite) ✓
|
|
32
|
+
* - Scroll, Linea ✓
|
|
33
|
+
* - Anvil (with --odyssey) ✓
|
|
34
|
+
* - Ethereum mainnet ✗ (until Pectra activates RIP-7212)
|
|
35
|
+
*
|
|
36
|
+
* Input layout: msgHash(32) || r(32) || s(32) || x(32) || y(32)
|
|
37
|
+
* Output: bool — true iff the signature verifies.
|
|
38
|
+
*/
|
|
39
|
+
library P256Verifier {
|
|
40
|
+
address internal constant RIP7212_PRECOMPILE = address(0x100);
|
|
41
|
+
|
|
42
|
+
function verify(bytes32 hash, uint256 r, uint256 s, uint256 x, uint256 y) internal view returns (bool) {
|
|
43
|
+
bytes memory input = abi.encodePacked(hash, r, s, x, y);
|
|
44
|
+
(bool ok, bytes memory out) = RIP7212_PRECOMPILE.staticcall(input);
|
|
45
|
+
return ok && out.length >= 32 && uint256(bytes32(out)) == 1;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.28;
|
|
3
|
+
|
|
4
|
+
import "../ApprovedHashRegistry.sol";
|
|
5
|
+
import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol";
|
|
6
|
+
import {WebAuthnLib} from "./WebAuthnLib.sol";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @title SignatureSlotRecovery
|
|
10
|
+
* @notice Safe-compatible 65-byte signature-slot recovery, factored out
|
|
11
|
+
* of `QuorumEnforcer` so both the caveat path (via
|
|
12
|
+
* `QuorumEnforcer.beforeHook`) and the account-direct path
|
|
13
|
+
* (`AgentAccount.proposeAdmin` / `executeAdmin` / `cancelAdmin`)
|
|
14
|
+
* can verify quorum signatures without duplicating ~40 lines
|
|
15
|
+
* of ECDSA / ERC-1271 / approve-hash branching.
|
|
16
|
+
*
|
|
17
|
+
* @dev Per-slot layout (65 bytes):
|
|
18
|
+
* {32 r/data}{32 s/data}{1 v/type}
|
|
19
|
+
*
|
|
20
|
+
* v-byte type discrimination (Safe-compatible + AP extension):
|
|
21
|
+
* v == 27 || v == 28 → ECDSA over `payloadHash`
|
|
22
|
+
* v > 30 → eth_sign ECDSA (EIP-191 wrapped); v - 4 = recovery
|
|
23
|
+
* v == 1 → pre-approved hash via `approvedHashRegistry`;
|
|
24
|
+
* r holds signer (left-padded), s unused
|
|
25
|
+
* v == 0 → ERC-1271 contract sig; r holds signer
|
|
26
|
+
* (left-padded), s holds the byte offset into
|
|
27
|
+
* `signatures` to a length-prefixed sig tail
|
|
28
|
+
* v == 2 → WebAuthn passkey sig (agenticprimitives
|
|
29
|
+
* extension; NOT in Safe). r holds the
|
|
30
|
+
* Passkey-Identity-Address (PIA, derived
|
|
31
|
+
* from the P-256 pubkey via
|
|
32
|
+
* `address(uint160(uint256(keccak256(abi.encode(x, y)))))`).
|
|
33
|
+
* s holds the byte offset into `signatures`
|
|
34
|
+
* to a length-prefixed tail containing
|
|
35
|
+
* `abi.encode(uint256 x, uint256 y, bytes32 rpIdHash, WebAuthnLib.Assertion assertion)`.
|
|
36
|
+
* The `rpIdHash` MUST equal the rpIdHash this credential was
|
|
37
|
+
* registered against — verified by `WebAuthnLib.verify`. Added
|
|
38
|
+
* in H7-C.1 / CON-WEBAUTHN-001 closure.
|
|
39
|
+
* The library re-derives the PIA from (x, y)
|
|
40
|
+
* and asserts it matches r before running
|
|
41
|
+
* the WebAuthn verify against (x, y).
|
|
42
|
+
* Caller-side authorization is unchanged —
|
|
43
|
+
* CustodyPolicy still checks
|
|
44
|
+
* `account.isCustodian(pia)` after recovery.
|
|
45
|
+
*
|
|
46
|
+
* Library is `internal` so the bodies inline into each calling contract
|
|
47
|
+
* (no DELEGATECALL hop, no proxy address juggling, no extra storage
|
|
48
|
+
* lookup). Both callers pay the gas for one copy each but the
|
|
49
|
+
* maintenance burden of "two diverging recovers" is eliminated.
|
|
50
|
+
*/
|
|
51
|
+
library SignatureSlotRecovery {
|
|
52
|
+
error InvalidSignature(uint8 v);
|
|
53
|
+
error ApprovedHashRequired(address signer);
|
|
54
|
+
error ContractSigInvalid(address signer);
|
|
55
|
+
error PasskeySigInvalid(address signer);
|
|
56
|
+
error PasskeyPubKeyMismatch(address claimed, address derived);
|
|
57
|
+
/// @notice H7-C.3 / CON-SIG-SLOT-001/-002 — a slot's tail (length + blob)
|
|
58
|
+
/// claims to extend past the end of the `signatures` array.
|
|
59
|
+
/// Without this check, the assembly load could read unallocated
|
|
60
|
+
/// memory and pass garbage to the downstream verifier.
|
|
61
|
+
error SigTailOutOfBounds(uint8 v, uint256 sigOffset, uint256 sigLen, uint256 totalLen);
|
|
62
|
+
|
|
63
|
+
bytes4 internal constant ERC1271_MAGIC = 0x1626ba7e;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @notice Recover the signer for the i-th 65-byte slot in
|
|
67
|
+
* `signatures`. Reverts with one of the library errors on
|
|
68
|
+
* malformed input or failed sub-checks; returns the signer
|
|
69
|
+
* address on success.
|
|
70
|
+
*
|
|
71
|
+
* @param payloadHash The hash the signer signed.
|
|
72
|
+
* @param signatures Packed sig blob (n * 65 bytes + optional
|
|
73
|
+
* ERC-1271 sig tails appended after).
|
|
74
|
+
* @param index Zero-based slot index to recover.
|
|
75
|
+
* @param approvedHashRegistry Address of the v=1 path's companion
|
|
76
|
+
* registry. Pass `address(0)` if the
|
|
77
|
+
* caller does not want to allow v=1 sigs
|
|
78
|
+
* (e.g. quorum-of-owners admin paths
|
|
79
|
+
* that don't accept pre-approved hashes).
|
|
80
|
+
*/
|
|
81
|
+
function recoverFromSlot(
|
|
82
|
+
bytes32 payloadHash,
|
|
83
|
+
bytes memory signatures,
|
|
84
|
+
uint256 index,
|
|
85
|
+
address approvedHashRegistry
|
|
86
|
+
) internal view returns (address signer) {
|
|
87
|
+
bytes32 r;
|
|
88
|
+
bytes32 s;
|
|
89
|
+
uint8 v;
|
|
90
|
+
uint256 offset = index * 65;
|
|
91
|
+
assembly {
|
|
92
|
+
let pos := add(signatures, add(0x20, offset))
|
|
93
|
+
r := mload(pos)
|
|
94
|
+
s := mload(add(pos, 0x20))
|
|
95
|
+
v := byte(0, mload(add(pos, 0x40)))
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (v == 0) {
|
|
99
|
+
// ERC-1271 contract signature. r holds the signer
|
|
100
|
+
// (left-padded); s holds the offset into `signatures` to a
|
|
101
|
+
// (length, blob) tail.
|
|
102
|
+
signer = address(uint160(uint256(r)));
|
|
103
|
+
uint256 sigOffset = uint256(s);
|
|
104
|
+
// H7-C.3 / CON-SIG-SLOT-001: bound the length-prefix read.
|
|
105
|
+
// sigOffset must leave room for the 32-byte length word.
|
|
106
|
+
if (sigOffset + 32 > signatures.length) {
|
|
107
|
+
revert SigTailOutOfBounds(v, sigOffset, 0, signatures.length);
|
|
108
|
+
}
|
|
109
|
+
uint256 sigLen;
|
|
110
|
+
assembly {
|
|
111
|
+
sigLen := mload(add(signatures, add(0x20, sigOffset)))
|
|
112
|
+
}
|
|
113
|
+
// H7-C.3: bound the tail-blob copy. The tail (length + blob) must
|
|
114
|
+
// fit fully within `signatures.length`; otherwise the assembly
|
|
115
|
+
// copy below reads past the buffer and feeds garbage to the
|
|
116
|
+
// verifier.
|
|
117
|
+
if (sigOffset + 32 + sigLen > signatures.length) {
|
|
118
|
+
revert SigTailOutOfBounds(v, sigOffset, sigLen, signatures.length);
|
|
119
|
+
}
|
|
120
|
+
bytes memory dyn = new bytes(sigLen);
|
|
121
|
+
assembly {
|
|
122
|
+
let src := add(signatures, add(0x40, sigOffset))
|
|
123
|
+
let dst := add(dyn, 0x20)
|
|
124
|
+
for { let j := 0 } lt(j, sigLen) { j := add(j, 0x20) } {
|
|
125
|
+
mstore(add(dst, j), mload(add(src, j)))
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
try IERC1271(signer).isValidSignature(payloadHash, dyn) returns (bytes4 magic) {
|
|
129
|
+
if (magic != ERC1271_MAGIC) revert ContractSigInvalid(signer);
|
|
130
|
+
} catch {
|
|
131
|
+
revert ContractSigInvalid(signer);
|
|
132
|
+
}
|
|
133
|
+
} else if (v == 1) {
|
|
134
|
+
// Pre-approved hash.
|
|
135
|
+
if (approvedHashRegistry == address(0)) revert InvalidSignature(v);
|
|
136
|
+
signer = address(uint160(uint256(r)));
|
|
137
|
+
if (!ApprovedHashRegistry(approvedHashRegistry).isApproved(signer, payloadHash)) {
|
|
138
|
+
revert ApprovedHashRequired(signer);
|
|
139
|
+
}
|
|
140
|
+
} else if (v == 2) {
|
|
141
|
+
// WebAuthn passkey slot. r holds the claimed PIA; tail
|
|
142
|
+
// holds (x, y, rpIdHash, Assertion). Verify the assertion
|
|
143
|
+
// against the supplied pubkey, then assert that the PIA
|
|
144
|
+
// derived from (x, y) matches the claim. Caller authorizes
|
|
145
|
+
// via `account.isCustodian(signer)`.
|
|
146
|
+
signer = address(uint160(uint256(r)));
|
|
147
|
+
uint256 sigOffset = uint256(s);
|
|
148
|
+
// H7-C.3 / CON-SIG-SLOT-002: bound the length-prefix read.
|
|
149
|
+
if (sigOffset + 32 > signatures.length) {
|
|
150
|
+
revert SigTailOutOfBounds(v, sigOffset, 0, signatures.length);
|
|
151
|
+
}
|
|
152
|
+
uint256 sigLen;
|
|
153
|
+
assembly {
|
|
154
|
+
sigLen := mload(add(signatures, add(0x20, sigOffset)))
|
|
155
|
+
}
|
|
156
|
+
// H7-C.3: bound the tail-blob copy.
|
|
157
|
+
if (sigOffset + 32 + sigLen > signatures.length) {
|
|
158
|
+
revert SigTailOutOfBounds(v, sigOffset, sigLen, signatures.length);
|
|
159
|
+
}
|
|
160
|
+
bytes memory dyn = new bytes(sigLen);
|
|
161
|
+
assembly {
|
|
162
|
+
let src := add(signatures, add(0x40, sigOffset))
|
|
163
|
+
let dst := add(dyn, 0x20)
|
|
164
|
+
for { let j := 0 } lt(j, sigLen) { j := add(j, 0x20) } {
|
|
165
|
+
mstore(add(dst, j), mload(add(src, j)))
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// H7-C.1 / CON-WEBAUTHN-001: slot now carries rpIdHash so the
|
|
169
|
+
// verifier pins the RP the assertion was produced for.
|
|
170
|
+
// Slot encoding: (uint256 pubX, uint256 pubY, bytes32 rpIdHash,
|
|
171
|
+
// WebAuthnLib.Assertion assertion).
|
|
172
|
+
(uint256 pubX, uint256 pubY, bytes32 rpIdHash, WebAuthnLib.Assertion memory assertion) =
|
|
173
|
+
abi.decode(dyn, (uint256, uint256, bytes32, WebAuthnLib.Assertion));
|
|
174
|
+
address derived = address(uint160(uint256(keccak256(abi.encode(pubX, pubY)))));
|
|
175
|
+
if (derived != signer) revert PasskeyPubKeyMismatch(signer, derived);
|
|
176
|
+
// requireUv=false at the slot level; account policy can layer UV
|
|
177
|
+
// requirement via a future policy module.
|
|
178
|
+
if (!WebAuthnLib.verify(assertion, payloadHash, pubX, pubY, rpIdHash, false)) {
|
|
179
|
+
revert PasskeySigInvalid(signer);
|
|
180
|
+
}
|
|
181
|
+
} else if (v == 27 || v == 28) {
|
|
182
|
+
signer = ecrecover(payloadHash, v, r, s);
|
|
183
|
+
if (signer == address(0)) revert InvalidSignature(v);
|
|
184
|
+
} else if (v > 30) {
|
|
185
|
+
// eth_sign-wrapped: signer prefixed with the "Ethereum Signed Message"
|
|
186
|
+
// wrapper before signing. Subtract 4 from v to recover the original
|
|
187
|
+
// recovery byte.
|
|
188
|
+
bytes32 wrapped =
|
|
189
|
+
keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", payloadHash));
|
|
190
|
+
signer = ecrecover(wrapped, v - 4, r, s);
|
|
191
|
+
if (signer == address(0)) revert InvalidSignature(v);
|
|
192
|
+
} else {
|
|
193
|
+
revert InvalidSignature(v);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.28;
|
|
3
|
+
|
|
4
|
+
import "./P256Verifier.sol";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @title WebAuthnLib
|
|
8
|
+
* @notice Pure-computation library for verifying a WebAuthn assertion against
|
|
9
|
+
* an expected challenge hash and a registered P-256 public key.
|
|
10
|
+
*
|
|
11
|
+
* Reconstructs the signing message:
|
|
12
|
+
* signingMessage = authenticatorData || sha256(clientDataJSON)
|
|
13
|
+
* signingHash = sha256(signingMessage)
|
|
14
|
+
*
|
|
15
|
+
* Checks (H7-C.1 / CON-WEBAUTHN-001 closure):
|
|
16
|
+
* - `authData[0..32]` (rpIdHash) EQUALS `expectedRpIdHash`
|
|
17
|
+
* — kills cross-RP signing-oracle attacks where an attacker controls
|
|
18
|
+
* a signing oracle at a different RP whose origin happens to be
|
|
19
|
+
* under their control. WITHOUT this check the on-chain verifier
|
|
20
|
+
* accepted any P-256 signature over `sha256(authData || sha256(cdj))`
|
|
21
|
+
* regardless of which RP produced it.
|
|
22
|
+
* - User-Present bit set (`authData[32] & 0x01 == 0x01`).
|
|
23
|
+
* - If `requireUv = true`, User-Verified bit set (`authData[32] & 0x04 == 0x04`).
|
|
24
|
+
* - `clientDataJSON` contains `"type":"webauthn.get"` at `typeIndex`.
|
|
25
|
+
* - `clientDataJSON` contains `"challenge":"<base64url(expectedHash)>"`
|
|
26
|
+
* at `challengeIndex`.
|
|
27
|
+
* - `P256Verifier.verify(signingHash, r, s, x, y)` returns true.
|
|
28
|
+
*
|
|
29
|
+
* The `clientDataJSON.origin` allowlist is NOT pinned here. Origin
|
|
30
|
+
* semantics depend on multi-frontend account policy and live in the
|
|
31
|
+
* account's stored policy if it adopts an allowlist — verifiers that
|
|
32
|
+
* want origin-pinning supply a pre-hashed origin via the consumer's
|
|
33
|
+
* own logic.
|
|
34
|
+
*
|
|
35
|
+
* Used by both AgentAccount (native passkey path) and the standalone
|
|
36
|
+
* PasskeyValidator (for external 1271-style verification).
|
|
37
|
+
*/
|
|
38
|
+
library WebAuthnLib {
|
|
39
|
+
/// @notice A WebAuthn-wrapped P-256 signature bundle.
|
|
40
|
+
struct Assertion {
|
|
41
|
+
bytes authenticatorData;
|
|
42
|
+
string clientDataJSON;
|
|
43
|
+
uint256 challengeIndex;
|
|
44
|
+
uint256 typeIndex;
|
|
45
|
+
uint256 r;
|
|
46
|
+
uint256 s;
|
|
47
|
+
bytes32 credentialIdDigest;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// @notice Verify that `assertion` is a valid WebAuthn signature over
|
|
51
|
+
/// `expectedChallengeHash` produced by `(pubX, pubY)`, AND was
|
|
52
|
+
/// produced for the RP whose ID hashes to `expectedRpIdHash`,
|
|
53
|
+
/// AND has the User-Present (and optionally User-Verified) bits set.
|
|
54
|
+
///
|
|
55
|
+
/// @param assertion The decoded WebAuthn assertion bundle.
|
|
56
|
+
/// @param expectedChallengeHash The 32-byte challenge the assertion must sign.
|
|
57
|
+
/// @param pubX, pubY The registered P-256 public key (uncompressed).
|
|
58
|
+
/// @param expectedRpIdHash `sha256(rpId)` of the RP the credential was
|
|
59
|
+
/// registered against. Pinning this kills the
|
|
60
|
+
/// cross-RP signing-oracle vector.
|
|
61
|
+
/// @param requireUv Whether the User-Verified bit must be set
|
|
62
|
+
/// (the account's policy choice).
|
|
63
|
+
function verify(
|
|
64
|
+
Assertion memory assertion,
|
|
65
|
+
bytes32 expectedChallengeHash,
|
|
66
|
+
uint256 pubX,
|
|
67
|
+
uint256 pubY,
|
|
68
|
+
bytes32 expectedRpIdHash,
|
|
69
|
+
bool requireUv
|
|
70
|
+
) internal view returns (bool) {
|
|
71
|
+
if (!_checkAuthData(assertion.authenticatorData, expectedRpIdHash, requireUv)) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
if (!_checkClientData(assertion.clientDataJSON, assertion.typeIndex, assertion.challengeIndex, expectedChallengeHash)) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
bytes32 cdjHash = sha256(bytes(assertion.clientDataJSON));
|
|
78
|
+
bytes32 signingHash = sha256(abi.encodePacked(assertion.authenticatorData, cdjHash));
|
|
79
|
+
return P256Verifier.verify(signingHash, assertion.r, assertion.s, pubX, pubY);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// @dev Validates the authenticatorData header per WebAuthn spec §6.1:
|
|
83
|
+
/// bytes [0..32] = rpIdHash
|
|
84
|
+
/// byte [32] = flags (bit 0 UP, bit 2 UV, bit 6 AT, bit 7 ED)
|
|
85
|
+
/// bytes [33..36] = signCount (big-endian u32; informational, not checked here)
|
|
86
|
+
function _checkAuthData(
|
|
87
|
+
bytes memory authData,
|
|
88
|
+
bytes32 expectedRpIdHash,
|
|
89
|
+
bool requireUv
|
|
90
|
+
) private pure returns (bool) {
|
|
91
|
+
if (authData.length < 37) return false;
|
|
92
|
+
// Compare 32-byte rpIdHash prefix.
|
|
93
|
+
bytes32 actualRpIdHash;
|
|
94
|
+
assembly {
|
|
95
|
+
// authData is `bytes`; first 32 bytes after the length word are the rpIdHash.
|
|
96
|
+
actualRpIdHash := mload(add(authData, 0x20))
|
|
97
|
+
}
|
|
98
|
+
if (actualRpIdHash != expectedRpIdHash) return false;
|
|
99
|
+
uint8 flags = uint8(authData[32]);
|
|
100
|
+
if (flags & 0x01 == 0) return false; // UP not set
|
|
101
|
+
if (requireUv && flags & 0x04 == 0) return false; // UV required but not set
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/// @dev Validates the two structural invariants of clientDataJSON:
|
|
106
|
+
/// (1) starts with `"type":"webauthn.get"` at typeIndex
|
|
107
|
+
/// (2) has `"challenge":"<base64url(hash)>"` at challengeIndex.
|
|
108
|
+
function _checkClientData(
|
|
109
|
+
string memory cdj,
|
|
110
|
+
uint256 typeIndex,
|
|
111
|
+
uint256 challengeIndex,
|
|
112
|
+
bytes32 hash
|
|
113
|
+
) private pure returns (bool) {
|
|
114
|
+
bytes memory buf = bytes(cdj);
|
|
115
|
+
bytes memory typeExpected = bytes('"type":"webauthn.get"');
|
|
116
|
+
if (typeIndex + typeExpected.length > buf.length) return false;
|
|
117
|
+
for (uint256 i; i < typeExpected.length; i++) {
|
|
118
|
+
if (buf[typeIndex + i] != typeExpected[i]) return false;
|
|
119
|
+
}
|
|
120
|
+
bytes memory challengePrefix = bytes('"challenge":"');
|
|
121
|
+
if (challengeIndex + challengePrefix.length + 43 + 1 > buf.length) return false;
|
|
122
|
+
for (uint256 i; i < challengePrefix.length; i++) {
|
|
123
|
+
if (buf[challengeIndex + i] != challengePrefix[i]) return false;
|
|
124
|
+
}
|
|
125
|
+
if (buf[challengeIndex + challengePrefix.length + 43] != bytes1('"')) return false;
|
|
126
|
+
|
|
127
|
+
// Decode the 43 base64url chars that follow the prefix and compare to `hash`.
|
|
128
|
+
return _base64UrlEqualsHash(buf, challengeIndex + challengePrefix.length, hash);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/// @dev Read 43 base64url chars starting at `offset` in `buf`, decode, and
|
|
132
|
+
/// assert the 32 decoded bytes equal `hash`.
|
|
133
|
+
function _base64UrlEqualsHash(bytes memory buf, uint256 offset, bytes32 hash) private pure returns (bool) {
|
|
134
|
+
uint256 acc;
|
|
135
|
+
uint256 bits;
|
|
136
|
+
uint256 outIdx;
|
|
137
|
+
bytes memory decoded = new bytes(32);
|
|
138
|
+
for (uint256 i; i < 43; i++) {
|
|
139
|
+
int256 v = _b64UrlCharVal(buf[offset + i]);
|
|
140
|
+
if (v < 0) return false;
|
|
141
|
+
acc = (acc << 6) | uint256(v);
|
|
142
|
+
bits += 6;
|
|
143
|
+
while (bits >= 8 && outIdx < 32) {
|
|
144
|
+
bits -= 8;
|
|
145
|
+
decoded[outIdx++] = bytes1(uint8((acc >> bits) & 0xff));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (outIdx != 32) return false;
|
|
149
|
+
for (uint256 i; i < 32; i++) {
|
|
150
|
+
if (decoded[i] != hash[i]) return false;
|
|
151
|
+
}
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function _b64UrlCharVal(bytes1 c) private pure returns (int256) {
|
|
156
|
+
uint8 b = uint8(c);
|
|
157
|
+
if (b >= 0x41 && b <= 0x5a) return int256(uint256(b - 0x41));
|
|
158
|
+
if (b >= 0x61 && b <= 0x7a) return int256(uint256(b - 0x61 + 26));
|
|
159
|
+
if (b >= 0x30 && b <= 0x39) return int256(uint256(b - 0x30 + 52));
|
|
160
|
+
if (b == 0x2d) return 62;
|
|
161
|
+
if (b == 0x5f) return 63;
|
|
162
|
+
return -1;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.28;
|
|
3
|
+
|
|
4
|
+
import "./AgentNameRegistry.sol";
|
|
5
|
+
import "../ontology/AttributeStorage.sol";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @title AgentNameAttributeResolver
|
|
9
|
+
* @notice Per-node records resolver for the agent-naming registry.
|
|
10
|
+
*
|
|
11
|
+
* Inherits `AttributeStorage` so every (node, predicate) write is
|
|
12
|
+
* validated against the bound `OntologyTermRegistry` — predicates MUST
|
|
13
|
+
* be registered + active before they can be stored. The subject is the
|
|
14
|
+
* namehash node (already `bytes32`, no conversion). The on-chain
|
|
15
|
+
* `AgentName` shape (defined in `ShapeRegistry`) governs which
|
|
16
|
+
* predicates / datatypes / cardinalities are well-formed for a name.
|
|
17
|
+
*
|
|
18
|
+
* Authorization: `msg.sender == REGISTRY.owner(node)`. The owner Smart
|
|
19
|
+
* Agent's CustodyPolicy module gates the call upstream — this contract
|
|
20
|
+
* trusts `msg.sender` to be the gated entity per spec 215 § Phase 3.
|
|
21
|
+
*
|
|
22
|
+
* Per-spec simplifications kept from the pure key/value port:
|
|
23
|
+
* - No multi-coin (ENSIP-9) address records. Other-chain identifiers
|
|
24
|
+
* ride on the `atl:nativeId` predicate (a CAIP-10 string).
|
|
25
|
+
* - No aliases, versioning, or operator approvals. Rotation flows
|
|
26
|
+
* through the registry's `setOwner` (which changes who may write).
|
|
27
|
+
*
|
|
28
|
+
* SDK contract: `agenticprimitives/agent-naming/records` exposes
|
|
29
|
+
* the same predicate ids and routes encode / decode through the typed
|
|
30
|
+
* setters here. See `AgentNamePredicates.sol` for the canonical set.
|
|
31
|
+
*/
|
|
32
|
+
contract AgentNameAttributeResolver is AttributeStorage {
|
|
33
|
+
AgentNameRegistry public immutable REGISTRY;
|
|
34
|
+
|
|
35
|
+
error NotAuthorized();
|
|
36
|
+
error NodeNotFound();
|
|
37
|
+
|
|
38
|
+
constructor(AgentNameRegistry registry, address ontology) AttributeStorage(ontology) {
|
|
39
|
+
REGISTRY = registry;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ─── Typed setters (predicate-active checked + owner-only) ──────
|
|
43
|
+
|
|
44
|
+
function setStringAttribute(bytes32 node, bytes32 predicate, string calldata value) external {
|
|
45
|
+
_requireAuth(node);
|
|
46
|
+
_setString(node, predicate, value);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function setAddressAttribute(bytes32 node, bytes32 predicate, address value) external {
|
|
50
|
+
_requireAuth(node);
|
|
51
|
+
_setAddress(node, predicate, value);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function setBoolAttribute(bytes32 node, bytes32 predicate, bool value) external {
|
|
55
|
+
_requireAuth(node);
|
|
56
|
+
_setBool(node, predicate, value);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function setUintAttribute(bytes32 node, bytes32 predicate, uint256 value) external {
|
|
60
|
+
_requireAuth(node);
|
|
61
|
+
_setUint(node, predicate, value);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function setBytes32Attribute(bytes32 node, bytes32 predicate, bytes32 value) external {
|
|
65
|
+
_requireAuth(node);
|
|
66
|
+
_setBytes32(node, predicate, value);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function setStringArrayAttribute(bytes32 node, bytes32 predicate, string[] calldata values) external {
|
|
70
|
+
_requireAuth(node);
|
|
71
|
+
_setStringArr(node, predicate, values);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function setAddressArrayAttribute(bytes32 node, bytes32 predicate, address[] calldata values) external {
|
|
75
|
+
_requireAuth(node);
|
|
76
|
+
_setAddressArr(node, predicate, values);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function setBytes32ArrayAttribute(bytes32 node, bytes32 predicate, bytes32[] calldata values) external {
|
|
80
|
+
_requireAuth(node);
|
|
81
|
+
_setBytes32Arr(node, predicate, values);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function unsetAttribute(bytes32 node, bytes32 predicate) external {
|
|
85
|
+
_requireAuth(node);
|
|
86
|
+
_unset(node, predicate);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ─── Auth ───────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
function _requireAuth(bytes32 node) internal view {
|
|
92
|
+
if (!REGISTRY.recordExists(node)) revert NodeNotFound();
|
|
93
|
+
if (msg.sender != REGISTRY.owner(node)) revert NotAuthorized();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.28;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @title AgentNamePredicates
|
|
6
|
+
* @notice Canonical bytes32 predicate ids for the AgentName ontology
|
|
7
|
+
* (`atl:*` CURIEs). Single source of truth shared by:
|
|
8
|
+
* - Deploy.s.sol (registers these in OntologyTermRegistry)
|
|
9
|
+
* - AgentNameAttributeResolver (typed-attribute setters
|
|
10
|
+
* validate against these ids)
|
|
11
|
+
* - agenticprimitives/agent-naming SDK (off-chain encoders /
|
|
12
|
+
* decoders use the same ids)
|
|
13
|
+
*
|
|
14
|
+
* Naming convention: `ATL_<UPPER_SNAKE_NAME>` where the CURIE is
|
|
15
|
+
* `atl:<lowerCamelName>`. Use a library so the constants can be
|
|
16
|
+
* imported as `using AgentNamePredicates for *;` OR referenced
|
|
17
|
+
* directly as `AgentNamePredicates.ATL_DISPLAY_NAME`.
|
|
18
|
+
*/
|
|
19
|
+
library AgentNamePredicates {
|
|
20
|
+
// ─── Resolver-record predicates (forward record + typed metadata) ─
|
|
21
|
+
|
|
22
|
+
/// `atl:addr` — the forward record. Stored as `address` datatype.
|
|
23
|
+
bytes32 internal constant ATL_ADDR = keccak256("atl:addr");
|
|
24
|
+
|
|
25
|
+
/// `atl:agentKind` — discriminator (`person`/`org`/`service`/`treasury`).
|
|
26
|
+
/// Stored as `bytes32` (hashed enum value). Bound to AGENT_KIND_ENUM in ShapeRegistry.
|
|
27
|
+
bytes32 internal constant ATL_AGENT_KIND = keccak256("atl:agentKind");
|
|
28
|
+
|
|
29
|
+
/// `atl:displayName` — human-friendly label. `string`.
|
|
30
|
+
bytes32 internal constant ATL_DISPLAY_NAME = keccak256("atl:displayName");
|
|
31
|
+
|
|
32
|
+
/// `atl:a2aEndpoint` — A2A service URL. `string`.
|
|
33
|
+
bytes32 internal constant ATL_A2A_ENDPOINT = keccak256("atl:a2aEndpoint");
|
|
34
|
+
|
|
35
|
+
/// `atl:mcpEndpoint` — MCP service URL. `string`.
|
|
36
|
+
bytes32 internal constant ATL_MCP_ENDPOINT = keccak256("atl:mcpEndpoint");
|
|
37
|
+
|
|
38
|
+
/// `atl:metadataURI` — off-chain JSON profile URL. `string`.
|
|
39
|
+
bytes32 internal constant ATL_METADATA_URI = keccak256("atl:metadataURI");
|
|
40
|
+
|
|
41
|
+
/// `atl:metadataHash` — keccak256 of the canonical-JSON profile
|
|
42
|
+
/// (matches `agenticprimitives/agent-identity.profileContentHash`).
|
|
43
|
+
/// `bytes32`.
|
|
44
|
+
bytes32 internal constant ATL_METADATA_HASH = keccak256("atl:metadataHash");
|
|
45
|
+
|
|
46
|
+
/// `atl:passkeyCredentialDigest` — keccak256 of the controlling
|
|
47
|
+
/// passkey credentialId (NEVER the raw credentialId). `bytes32`.
|
|
48
|
+
bytes32 internal constant ATL_PASSKEY_CREDENTIAL_DIGEST = keccak256("atl:passkeyCredentialDigest");
|
|
49
|
+
|
|
50
|
+
/// `atl:custodyPolicy` — address of the owner Smart Agent's
|
|
51
|
+
/// CustodyPolicy module. `address`.
|
|
52
|
+
bytes32 internal constant ATL_CUSTODY_POLICY = keccak256("atl:custodyPolicy");
|
|
53
|
+
|
|
54
|
+
/// `atl:nativeId` — CAIP-10 chain-agnostic account identifier
|
|
55
|
+
/// (per ADR-0008). `string`.
|
|
56
|
+
bytes32 internal constant ATL_NATIVE_ID = keccak256("atl:nativeId");
|
|
57
|
+
|
|
58
|
+
// ─── Shape + enum-set ids ───────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
/// `atl:AgentName` — the class id for ShapeRegistry validation.
|
|
61
|
+
bytes32 internal constant CLASS_AGENT_NAME = keccak256("atl:AgentName");
|
|
62
|
+
|
|
63
|
+
/// Enum set bound to `atl:agentKind`. Contains the three hashed
|
|
64
|
+
/// member ids below.
|
|
65
|
+
bytes32 internal constant AGENT_KIND_ENUM = keccak256("atl:AgentKindEnum");
|
|
66
|
+
|
|
67
|
+
// agentKind is 3-valued (person/org/service). A treasury is a KIND OF
|
|
68
|
+
// SERVICE (agentKind=service), distinguished at the profile layer
|
|
69
|
+
// (ProfileType/serviceType='treasury'; specs 210/217/225 §6) — NOT its own
|
|
70
|
+
// agent kind. Do not re-add AGENT_KIND_TREASURY.
|
|
71
|
+
bytes32 internal constant AGENT_KIND_PERSON = keccak256("person");
|
|
72
|
+
bytes32 internal constant AGENT_KIND_ORG = keccak256("org");
|
|
73
|
+
bytes32 internal constant AGENT_KIND_SERVICE = keccak256("service");
|
|
74
|
+
}
|