@@ -12,41 +12,54 @@ import {JsonClaim} from "./JsonClaim.sol";
1212contract RIK is ERC721 , Ownable {
1313 using RSA for bytes32 ;
1414
15+ string private constant _EXPECTED_ISS = "https://token.actions.githubusercontent.com " ;
16+
1517 struct RSAKey {
1618 bytes modulus;
1719 bytes exponent;
1820 bool active;
1921 }
2022
21- event KeyAdded (bytes32 indexed kid );
22- event KeyRevoked (bytes32 indexed kid );
23-
24- // store valid signing keys from GitHub -> they periodically rotate
25- mapping (bytes32 => RSAKey) private _keys;
26-
2723 struct Repo {
2824 uint64 githubRepoId; // == tokenId -> kept for clarity
2925 uint64 githubOwnerId;
3026 uint64 registeredAt; // block.timestamp -> truncated at uint64
3127 address registrant; // who called register()
3228 }
3329
34- // EVM log event used for off-chain indexing
30+ event KeyAdded (bytes32 indexed kid );
31+ event KeyRevoked (bytes32 indexed kid );
3532 event RepoRegistered (uint256 indexed repoId , address indexed registrant , uint64 githubOwnerId , uint64 registeredAt );
3633
37- mapping (uint256 => Repo) private _repos;
38-
39- // errors
4034 error AlreadyRegistered (uint256 repoId );
4135 error UnknownKid (bytes32 kid );
4236 error BadJwt ();
37+ error TokenExpired ();
38+ error TokenNotYetValid ();
39+ error NotRegistered (uint256 tokenId );
40+ error RepoIdTooLarge (uint256 repoId );
4341
44- // pin trusted issuer
45- // NOTE: make it so contract owner can update this
46- string public constant EXPECTED_ISS = " https://token.actions.githubusercontent.com " ;
42+ // store valid signing keys from GitHub; they periodically rotate
43+ mapping ( bytes32 => RSAKey) private _keys;
44+ mapping ( uint256 => Repo) private _repos ;
4745
46+ /**
47+ * @dev Sets the initial registry owner.
48+ */
4849 constructor (address initialOwner ) ERC721 ("Repository Identity Key " , "RIK " ) Ownable (initialOwner) {}
4950
51+ /**
52+ * @dev Registers a GitHub repository identity and mints its RIK to the caller.
53+ *
54+ * Requirements:
55+ *
56+ * - `kid` must identify an active GitHub Actions signing key.
57+ * - `signature` must be valid for `headerB64.payloadB64`.
58+ * - The JWT payload must match the caller, repository id, owner id, issuer, and active time window.
59+ * - `repoId` must not already be registered and must fit in the stored repository metadata.
60+ *
61+ * Emits a {RepoRegistered} event.
62+ */
5063 function register (
5164 bytes32 kid ,
5265 bytes calldata headerB64 ,
@@ -55,63 +68,114 @@ contract RIK is ERC721, Ownable {
5568 uint256 repoId ,
5669 uint64 githubOwnerId
5770 ) external {
71+ address registrant = _msgSender ();
5872 _verifyJwt (kid, headerB64, payloadB64, signature);
5973
6074 bytes memory payload = bytes (Base64.decode (string (payloadB64)));
75+ _verifyClaims (payload, registrant, repoId, githubOwnerId);
76+ _verifyActiveWindow (payload);
6177
62- // verify payload contains the right claim
63- JsonClaim.requireStringClaim (payload, "aud " , Strings.toHexString (uint160 (msg .sender ), 20 ));
64- JsonClaim.requireStringClaim (payload, "repository_id " , Strings.toString (repoId));
65- JsonClaim.requireStringClaim (payload, "repository_owner_id " , Strings.toString (uint256 (githubOwnerId)));
78+ _register (registrant, repoId, githubOwnerId);
79+ }
6680
67- // verify issuer
68- JsonClaim.requireStringClaim (payload, "iss " , EXPECTED_ISS);
81+ /**
82+ * @dev Returns the trusted GitHub Actions OIDC issuer.
83+ */
84+ function expectedIssuer () public pure virtual returns (string memory ) {
85+ return _EXPECTED_ISS;
86+ }
6987
70- // verify if JWT is expired
71- (uint256 exp_ , bool fe ) = _readUintClaim (payload, "exp " );
72- (uint256 nbf_ , bool fn ) = _readUintClaim (payload, "nbf " );
73- if (! fe || ! fn) revert JsonClaim.ClaimMissing ("exp/nbf " );
74- // JWT expiry must be checked against chain time; small validator timestamp drift is acceptable here.
75- // forge-lint: disable-next-line(block-timestamp)
76- if (block .timestamp > exp_) revert ("token expired " );
88+ /**
89+ * @dev Returns the RIK token id for a GitHub repository id.
90+ */
91+ function tokenIdOf (uint64 githubRepoId ) public pure virtual returns (uint256 ) {
92+ return uint256 (githubRepoId);
93+ }
7794
78- // JWT expiry must be checked against chain time; small validator timestamp drift is acceptable here.
79- // forge-lint: disable-next-line(block-timestamp)
80- if (block .timestamp < nbf_) revert ("token not yet valid " );
95+ /**
96+ * @dev Returns repository metadata for `tokenId`.
97+ *
98+ * Requirements:
99+ *
100+ * - `tokenId` must be registered.
101+ */
102+ function repoOf (uint256 tokenId ) public view virtual returns (Repo memory ) {
103+ if (_ownerOf (tokenId) == address (0 )) revert NotRegistered (tokenId);
104+ return _repos[tokenId];
105+ }
106+
107+ /**
108+ * @dev Adds or replaces an active GitHub Actions RSA signing key.
109+ *
110+ * Requirements:
111+ *
112+ * - The caller must be the contract owner.
113+ *
114+ * Emits a {KeyAdded} event.
115+ */
116+ function addKey (bytes32 kid , bytes calldata n , bytes calldata e ) external onlyOwner {
117+ _addKey (kid, n, e);
118+ }
119+
120+ /**
121+ * @dev Revokes a GitHub Actions RSA signing key.
122+ *
123+ * Requirements:
124+ *
125+ * - The caller must be the contract owner.
126+ *
127+ * Emits a {KeyRevoked} event.
128+ */
129+ function revokeKey (bytes32 kid ) external onlyOwner {
130+ _revokeKey (kid);
131+ }
81132
82- // prevent double registration
133+ /**
134+ * @dev Registers `repoId` to `registrant`.
135+ *
136+ * Requirements:
137+ *
138+ * - `repoId` must not already be registered.
139+ * - `repoId` must fit in the stored repository metadata.
140+ *
141+ * Emits a {RepoRegistered} event.
142+ */
143+ function _register (address registrant , uint256 repoId , uint64 githubOwnerId ) internal virtual {
83144 if (_ownerOf (repoId) != address (0 )) revert AlreadyRegistered (repoId);
145+ if (repoId > type (uint64 ).max) revert RepoIdTooLarge (repoId);
84146
85- Repo memory r = Repo ({
86- // safe to truncate -> collision is irrelevant
147+ // forge-lint: disable-next-line(unsafe-typecast)
148+ uint64 registeredAt = uint64 (block .timestamp );
149+ _repos[repoId] = Repo ({
150+ // casting is safe because repoId is bounded above.
87151 // forge-lint: disable-next-line(unsafe-typecast)
88152 githubRepoId: uint64 (repoId),
89153 githubOwnerId: githubOwnerId,
90- registeredAt: uint64 ( block . timestamp ) ,
91- registrant: msg . sender
154+ registeredAt: registeredAt ,
155+ registrant: registrant
92156 });
93- _repos[repoId] = r;
94157
95- emit RepoRegistered (repoId, msg . sender , githubOwnerId, r. registeredAt);
158+ emit RepoRegistered (repoId, registrant , githubOwnerId, registeredAt);
96159
97- _mint (msg . sender , repoId);
160+ _mint (registrant , repoId);
98161 }
99162
100- function tokenIdOf (uint64 githubRepoId ) external pure returns (uint256 ) {
101- return uint256 (githubRepoId);
102- }
103-
104- function repoOf (uint256 tokenId ) external view returns (Repo memory ) {
105- require (_ownerOf (tokenId) != address (0 ), "not registered " );
106- return _repos[tokenId];
107- }
108-
109- function addKey (bytes32 kid , bytes calldata n , bytes calldata e ) external onlyOwner {
163+ /**
164+ * @dev Adds or replaces an active GitHub Actions RSA signing key.
165+ *
166+ * Emits a {KeyAdded} event.
167+ */
168+ function _addKey (bytes32 kid , bytes calldata n , bytes calldata e ) internal virtual {
110169 _keys[kid] = RSAKey ({modulus: n, exponent: e, active: true });
111170 emit KeyAdded (kid);
112171 }
113172
114- function revokeKey (bytes32 kid ) external onlyOwner {
173+ /**
174+ * @dev Revokes a GitHub Actions RSA signing key.
175+ *
176+ * Emits a {KeyRevoked} event.
177+ */
178+ function _revokeKey (bytes32 kid ) internal virtual {
115179 _keys[kid].active = false ;
116180 emit KeyRevoked (kid);
117181 }
@@ -130,6 +194,35 @@ contract RIK is ERC721, Ownable {
130194 if (! RSA.pkcs1Sha256 (digest, signature, k.exponent, k.modulus)) revert BadJwt ();
131195 }
132196
197+ function _verifyClaims (bytes memory payload , address registrant , uint256 repoId , uint64 githubOwnerId )
198+ internal
199+ pure
200+ {
201+ JsonClaim.requireStringClaim (payload, "aud " , Strings.toHexString (uint160 (registrant), 20 ));
202+ JsonClaim.requireStringClaim (payload, "repository_id " , Strings.toString (repoId));
203+ JsonClaim.requireStringClaim (payload, "repository_owner_id " , Strings.toString (uint256 (githubOwnerId)));
204+ JsonClaim.requireStringClaim (payload, "iss " , _EXPECTED_ISS);
205+ }
206+
207+ function _verifyActiveWindow (bytes memory payload ) internal view {
208+ uint256 exp_ = _requireUintClaim (payload, "exp " );
209+ uint256 nbf_ = _requireUintClaim (payload, "nbf " );
210+
211+ // JWT validity must be checked against chain time; small validator timestamp drift is acceptable here.
212+ // forge-lint: disable-next-line(block-timestamp)
213+ if (block .timestamp > exp_) revert TokenExpired ();
214+
215+ // JWT validity must be checked against chain time; small validator timestamp drift is acceptable here.
216+ // forge-lint: disable-next-line(block-timestamp)
217+ if (block .timestamp < nbf_) revert TokenNotYetValid ();
218+ }
219+
220+ function _requireUintClaim (bytes memory payload , string memory key ) internal pure returns (uint256 value ) {
221+ bool found;
222+ (value, found) = _readUintClaim (payload, key);
223+ if (! found) revert JsonClaim.ClaimMissing (key);
224+ }
225+
133226 function _readUintClaim (bytes memory payload , string memory key ) internal pure returns (uint256 value , bool found ) {
134227 bytes memory marker = abi.encodePacked ('" ' , bytes (key), '": ' );
135228 int256 pos = JsonClaim.indexOf (payload, marker);
@@ -139,12 +232,15 @@ contract RIK is ERC721, Ownable {
139232 // casting to uint256 is safe because negative positions returned above.
140233 // forge-lint: disable-next-line(unsafe-typecast)
141234 uint256 i = uint256 (pos) + marker.length ;
235+ uint256 valueStart = i;
236+ if (i == payload.length ) return (0 , false );
237+
142238 while (i < payload.length ) {
143239 uint8 c = uint8 (payload[i]);
144240 if (c < 0x30 || c > 0x39 ) break ; // -> not a digit
145241 value = value * 10 + (c - 0x30 );
146242 ++ i;
147243 }
148- found = true ;
244+ found = i > valueStart ;
149245 }
150246}
0 commit comments