From fd06faf9c816ad9a86a8fd0a4f7c7f60205bed00 Mon Sep 17 00:00:00 2001 From: SupremaLex Date: Thu, 17 Sep 2026 18:00:30 +0300 Subject: [PATCH] feat(ceremony)!: github reveals its token request whole GitHub's exchange hid `client_secret` behind a range commitment and revealed the body up to it. It now reveals the request whole, the shape X already had: one revealed run from offset zero to the signed length, no commitment in that direction, every field a verifier reads in the open. `secretField: null` in profiles.json is the whole change; the generator takes GITHUB_TOKEN_SENT_COMMITMENTS to 0, `secret_field` to None and `secretField` to null from that one value. No verifier logic moves. An attestation of the exchange therefore publishes the application credential, to the notary that observed the session and to every reader of the chain. That is a property of the layout, and GitHub documents a public client as one that ships its secret. The fixtures are regenerated rather than edited: the deterministic record from libid-rs `ceremony_fixtures`, and a real MPC-TLS session captured against github.com. The X records are byte-identical, which is what says the change reaches only this profile. This changes what a `github/v1` verifier accepts: a record carrying the old committed suffix no longer verifies, and one carrying the new layout did not verify before. It is an edit to v1 rather than a new ceremony version on this evidence: chain-configurations registers no ceremony Platform Verifier on any network, so no github/v1 record has been accepted anywhere for the change to invalidate. Assisted-by: Claude Opus 5 Assisted-by: Claude Fable 5.1 Signed-off-by: SupremaLex --- rust/profiles/src/profiles.rs | 10 +- rust/profiles/tests/wire.rs | 2 +- .../ceremony/CeremonyAttestation.sol | 17 ++- .../contracts/ceremony/CeremonyProfile.sol | 2 +- .../ceremony/GitHubPlatformVerifier.sol | 61 +++++----- .../ceremony/TlsNotaryVerifierBase.sol | 19 ++-- solidity/contracts/ceremony/profiles.json | 27 +++-- .../test/GitHubPlatformVerifier.t.sol | 107 +++++++++--------- .../test/fixtures/github-ceremony-real.json | 14 +-- .../fixtures/github-ceremony-session.json | 18 +-- .../contracts/src/ceremony/profiles.ts | 10 +- 11 files changed, 135 insertions(+), 152 deletions(-) diff --git a/rust/profiles/src/profiles.rs b/rust/profiles/src/profiles.rs index d18ce49d..3d240f1c 100644 --- a/rust/profiles/src/profiles.rs +++ b/rust/profiles/src/profiles.rs @@ -139,10 +139,10 @@ pub const X: Profile = Profile { }), }; -/// A confidential client. The exchange runs in the deployment because the -/// secret cannot reach the browser, and the two sessions are served by -/// DIFFERENT hosts -- which is why one pinned authority per profile would -/// be wrong. +/// The credential GitHub calls `client_secret` is sent and revealed, so +/// an attestation publishes it and nothing here is confidential. The two +/// sessions are served by DIFFERENT hosts -- which is why one pinned +/// authority per profile would be wrong. pub const GITHUB: Profile = Profile { platform: "github", ceremony_version: 1, @@ -153,7 +153,7 @@ pub const GITHUB: Profile = Profile { path: "/login/oauth/access_token", request_line: "POST /login/oauth/access_token ", }, - secret_field: Some("client_secret"), + secret_field: None, required_headers: &[ "host: github.com", "content-type: application/x-www-form-urlencoded", diff --git a/rust/profiles/tests/wire.rs b/rust/profiles/tests/wire.rs index 1d5d68b1..38066c68 100644 --- a/rust/profiles/tests/wire.rs +++ b/rust/profiles/tests/wire.rs @@ -126,7 +126,7 @@ async fn the_x_token_request_head_is_one_the_profile_admits() { #[tokio::test] async fn the_github_exchange_head_is_one_the_profile_admits() { let session = GITHUB.token.expect("github notarizes a token session"); - // GitHub's body carries the secret last, per REQ-COMMON-22. + // GitHub's body carries the credential as its last field. let body = b"client_id=Iv1.abc&code=xyz&redirect_uri=https%3A%2F%2Fexample.test%2Fcb&code_verifier=5teBDl6cz4U77aFweV5PbMhBJ_lEFv6LLNKzqnDI5lo&client_secret=deadbeef"; let wire = head_hyper_writes(&session, body).await; assert_head_admits(&session, &wire, body.len()); diff --git a/solidity/contracts/ceremony/CeremonyAttestation.sol b/solidity/contracts/ceremony/CeremonyAttestation.sol index fdb4bf30..ab870058 100644 --- a/solidity/contracts/ceremony/CeremonyAttestation.sol +++ b/solidity/contracts/ceremony/CeremonyAttestation.sol @@ -25,12 +25,10 @@ import {CeremonyFields} from "./CeremonyFields.sol"; /// must contain belong to the Platform Verifier. /// /// IT DOES NOT CHECK COVERAGE. `decode` accepts a transcript byte covered -/// by neither a revealed range nor a commitment, because REQ-COMMON-35 is -/// conditional: it governs an identity-session request that commits a -/// credential in an `Authorization` header, and REQ-COMMON-43 explicitly -/// withholds it from a credential committed in a request body, which is -/// GitHub's `client_secret`. A library that tiled unconditionally would -/// reject every valid GitHub token exchange. +/// by neither a revealed range nor a commitment: which directions a +/// profile tiles is that profile's rule, and this library answers for the +/// encoding alone. Each Platform Verifier calls `requireExactCoverage` +/// for the directions its own profile accounts for. /// /// A gap is where a prover hides bytes, so the Platform Verifier of an /// identity session MUST call `requireExactCoverage` and MUST run the @@ -234,10 +232,9 @@ library CeremonyAttestation { /// `Authorization` header. /// /// @dev At launch that is X's `/2/users/me` request and GitHub's `/user` - /// request, and nothing else. REQ-COMMON-43 forbids applying these to - /// a credential committed in a request body -- GitHub's token - /// exchange commits `client_secret` in a form body, so demanding a - /// CRLF-framed header around it would reject every valid exchange. + /// request, and nothing else. A token request carries its credential + /// as a form field, where there is no header line to count and none + /// to frame, so none of the three reaches one. /// /// The three are one call because they are one property, and two of /// them are worthless alone. The uniqueness scan counts the needle diff --git a/solidity/contracts/ceremony/CeremonyProfile.sol b/solidity/contracts/ceremony/CeremonyProfile.sol index 8c3b471d..da6e8236 100644 --- a/solidity/contracts/ceremony/CeremonyProfile.sol +++ b/solidity/contracts/ceremony/CeremonyProfile.sol @@ -92,7 +92,7 @@ library CeremonyProfile { /// is one or zero and never a preference. uint256 internal constant X_TOKEN_SENT_COMMITMENTS = 0; - uint256 internal constant GITHUB_TOKEN_SENT_COMMITMENTS = 1; + uint256 internal constant GITHUB_TOKEN_SENT_COMMITMENTS = 0; /// @dev The two identity members a Platform Verifier reads out of the /// revealed response bytes (REQ-PLAT-51). diff --git a/solidity/contracts/ceremony/GitHubPlatformVerifier.sol b/solidity/contracts/ceremony/GitHubPlatformVerifier.sol index c1c76e78..e5bbbd6d 100644 --- a/solidity/contracts/ceremony/GitHubPlatformVerifier.sol +++ b/solidity/contracts/ceremony/GitHubPlatformVerifier.sol @@ -8,13 +8,10 @@ import {TlsNotaryVerifierBase} from "./TlsNotaryVerifierBase.sol"; /// @title GitHubPlatformVerifier — the `github/v1` profile. /// -/// @notice The same relation X states, from a confidential client. +/// @notice The same relation X states, for a second platform. /// -/// @dev GitHub uses a confidential client, so the exchange cannot run in the -/// browser and the deployment's Token-Exchange Service is the notarized -/// party for that session. On chain that changes nothing: this verifier -/// sees two attestations and one proof, exactly as X does. It differs in -/// four constants and two reads. +/// @dev This verifier sees two attestations and one proof, exactly as X does. +/// It differs in four constants and two reads. /// /// TWO AUTHORITIES, NOT ONE. `github.com` serves the exchange and /// `api.github.com` serves the identity read, so an authority is per @@ -27,36 +24,31 @@ import {TlsNotaryVerifierBase} from "./TlsNotaryVerifierBase.sol"; /// /// THE REVEALED LAYOUT IS A PROFILE DECISION, as it is for X: /// -/// token exchange — ONE revealed sent range, from offset 0 through the -/// body PREFIX; `_tokenBody` refuses any other count. -/// `client_secret` is ordered last per REQ-COMMON-22 -/// and committed, so the prefix ends where it begins -/// and every field a verifier reads sits inside it. +/// token exchange — ONE revealed sent range covering the request whole, +/// and NO commitment; `_tokenBody` refuses any other +/// count. The credential GitHub calls `client_secret` +/// is sent in the body and revealed with everything +/// around it, so every field a verifier reads lies in +/// the open and the request has no hidden suffix. /// -/// Nothing frames that commitment, unlike the bearer's: -/// its `&client_secret=` opener is on the hidden side, -/// so there is no revealed anchor to compare. A prover -/// starting it earlier hides a body suffix, and -/// `formField` cannot see a duplicate in there. What -/// closes that is ASM-PROV-07 -- the platform rejects a -/// body carrying a profile field twice -- backed by the -/// recurring probes REQ-COMMON-32 requires. Framing it -/// here would mean revealing `&client_secret=`, which -/// the profile's reveal table marks `no`, so it is a -/// specification question rather than a contract one. +/// What the revealed bytes do not settle is the +/// decoded form. `formField` refuses a name it finds +/// at two `&`-anchored positions, but a value carrying +/// a raw `&` or `=` would decode as fields nobody +/// counted. That is ASM-PROV-07 -- the platform +/// rejects a body carrying a profile field twice -- +/// backed by the recurring probes REQ-COMMON-32 +/// requires, exactly as it is for X. /// token response — the `"access_token":"` delimiter and closing quote /// revealed; the bearer and everything else committed. /// identity request — the bearer committed, every other byte revealed /// and tiled exactly. /// identity response — `id` and `login` with their full delimiters. /// -/// The three identity-session checks do NOT reach the exchange's body -/// credential: REQ-COMMON-43 forbids demanding a CRLF-framed -/// `authorization: Bearer ` around a form-body range, which would reject -/// every valid exchange. The secret is protected instead by being ordered -/// last with a delimiter-free charset, which REQ-PLAT-35 makes a -/// deployment obligation — GitHub secrets are hex, so it holds by -/// inspection. +/// An attestation of this exchange carries the application credential in +/// plaintext: to the notary that observed the session, and to every +/// reader of the chain that verifies it. That follows from the layout +/// above rather than from any choice a prover makes. contract GitHubPlatformVerifier is TlsNotaryVerifierBase { /// @custom:oz-upgrades-unsafe-allow constructor constructor() { @@ -103,8 +95,8 @@ contract GitHubPlatformVerifier is TlsNotaryVerifierBase { return CeremonyProfile.AUTHORITY_GITHUB_API; } - /// @dev GitHub commits its `client_secret`, ordered last under - /// REQ-COMMON-22, so exactly one committed range reaches the end. + /// @dev GitHub's exchange hides no body field, so its request is revealed + /// whole and this direction carries no commitment at all. function _tokenSentCommitments() internal pure override returns (uint256) { return CeremonyProfile.GITHUB_TOKEN_SENT_COMMITMENTS; } @@ -113,10 +105,9 @@ contract GitHubPlatformVerifier is TlsNotaryVerifierBase { return CeremonyProfile.GITHUB_TOKEN_REQUEST_LINE; } - /// @dev REQ-COMMON-21B: `host` and section 6.2's media type. The - /// Token-Exchange Service composes this request rather than a browser, - /// so these bytes are agreed here before that service is written; what - /// else it sends is not compared. + /// @dev REQ-COMMON-21B: `host` and section 6.2's media type. What else + /// the sender puts in the head is not compared: it changes what + /// GitHub answers, never how GitHub parses the body. function _tokenRequiredHeaders() internal pure override returns (bytes memory) { return CeremonyProfile.GITHUB_TOKEN_REQUIRED_HEADERS; } diff --git a/solidity/contracts/ceremony/TlsNotaryVerifierBase.sol b/solidity/contracts/ceremony/TlsNotaryVerifierBase.sol index f3402b62..ea844ab0 100644 --- a/solidity/contracts/ceremony/TlsNotaryVerifierBase.sol +++ b/solidity/contracts/ceremony/TlsNotaryVerifierBase.sol @@ -132,9 +132,9 @@ abstract contract TlsNotaryVerifierBase is IPlatformVerifier, PlatformVerifierBa /// `authorization`, and the framing around the committed value. function _tokenRequiredHeaders() internal pure virtual returns (bytes memory); - /// @dev How many committed ranges the token request carries. X hides no - /// body field and uses a public client, so zero; GitHub commits its - /// `client_secret`, ordered last, so one. + /// @dev How many committed ranges the token request carries. Both launch + /// profiles hide no body field, so both answer zero and their + /// requests are revealed whole. function _tokenSentCommitments() internal pure virtual returns (uint256); /// @dev Anything the profile checks in the token body beyond the fields @@ -469,9 +469,8 @@ abstract contract TlsNotaryVerifierBase is IPlatformVerifier, PlatformVerifierBa /// /// So the profile fixes the shape exactly: ONE revealed run beginning /// at offset 0, and exactly the committed ranges the profile expects. - /// X carries none, because it hides no body field and authenticates - /// with a public client; GitHub carries one, its `client_secret`, - /// ordered last under REQ-COMMON-22 and reaching the transcript end. + /// Both launch profiles expect none: the run covers the request + /// through to its signed length, so no body byte is hidden. /// /// AND THE HEAD ITSELF, byte for byte. Revealing the headers is not /// checking them: they were public and unconstrained here, while @@ -735,9 +734,11 @@ abstract contract TlsNotaryVerifierBase is IPlatformVerifier, PlatformVerifierBa uint256 declared = _checkTokenHead(_slice(whole, 0, at)); at += 4; - // `signedLength` is the whole request, and the head is revealed, so the - // remainder is the body -- GitHub's committed `client_secret` included, - // which the revealed run stops short of. + // `signedLength` is the whole request and the head is revealed, so the + // remainder is the body. It comes from the signed length rather than + // from the revealed run so that a profile committing part of its body + // is framed by what the notary signed; where a profile commits nothing + // the two are the same number. if (declared != signedLength - at) revert WrongDeclaredBodyLength(declared, signedLength - at); body = new bytes(whole.length - at); diff --git a/solidity/contracts/ceremony/profiles.json b/solidity/contracts/ceremony/profiles.json index a319a7b7..0758f98e 100644 --- a/solidity/contracts/ceremony/profiles.json +++ b/solidity/contracts/ceremony/profiles.json @@ -126,34 +126,37 @@ "ceremonyVersion": 1, "proofLifetimeSeconds": 3600, "note": [ - "A confidential client. The exchange runs in the deployment because the", - "secret cannot reach the browser, and the two sessions are served by", - "DIFFERENT hosts -- which is why one pinned authority per profile would", - "be wrong." + "The credential GitHub calls `client_secret` is sent and revealed, so", + "an attestation publishes it and nothing here is confidential. The two", + "sessions are served by DIFFERENT hosts -- which is why one pinned", + "authority per profile would be wrong." ], "sessions": { "token": { "authority": "github.com", "method": "POST", "path": "/login/oauth/access_token", - "secretField": "client_secret", + "secretField": null, "requiredHeaders": [ "host: github.com", "content-type: application/x-www-form-urlencoded" ], "note": [ - "The client secret is committed rather than revealed, ordered last", - "in the body under REQ-COMMON-22 so the committed run is a suffix", - "and not a hole.", + "The request is revealed whole, as X's is. The credential sits in", + "the body and is revealed with the rest of it, so the verifier", + "accepts exactly zero committed ranges in this direction.", + "", + "Which is why the media type is required. Revealed is not checked:", + "it was public and unconstrained until the verifier compared it,", + "and it decides how GitHub parses the very bytes `formField`", + "reads.", "", "The media type is section 6.2's and `host` names the authority", - "the notary authenticated. The Token-Exchange Service also sends", + "the notary authenticated. Whoever sends this request also sends", "`accept: application/json`, without which GitHub answers", "form-encoded and the verifier finds no `\"access_token\":\"`, and", "`connection: close`, which ends every notarized session here;", - "neither is compared. The service is the party that sends this", - "request, so unlike X's it is agreed here BEFORE it is written", - "rather than read off a browser that already sends it." + "neither is compared." ] }, "identity": { diff --git a/solidity/contracts/ceremony/test/GitHubPlatformVerifier.t.sol b/solidity/contracts/ceremony/test/GitHubPlatformVerifier.t.sol index 4aec45c4..1be312c1 100644 --- a/solidity/contracts/ceremony/test/GitHubPlatformVerifier.t.sol +++ b/solidity/contracts/ceremony/test/GitHubPlatformVerifier.t.sol @@ -110,32 +110,34 @@ contract GitHubPlatformVerifierTest is Test { ); } - /// The exchange: request line, then the revealed body PREFIX. The secret is - /// ordered last and committed, so the prefix stops where it begins. - /// The exchange request as the profile fixes it: one revealed run up to - /// the secret, which is ordered last and committed. The head boundary sits - /// inside the revealed run, so the body is located by the framing rather - /// than by a range position. - function _exchangeSent() private view returns (AttestationBuilder.Direction memory) { - bytes memory prefix = abi.encodePacked( + /// The exchange body: the five fields the profile lists, the credential + /// among them and revealed like the rest. + function _exchangeBody() private view returns (bytes memory) { + return abi.encodePacked( "client_id=Iv1.8a61f9b3a7aba766&code=abc&redirect_uri=https%3A%2F%2Fa.example&code_verifier=", - CeremonyAuthorization.codeVerifier(digest, AUTH_NONCE) + CeremonyAuthorization.codeVerifier(digest, AUTH_NONCE), + "&client_secret=0123456789abcdef0123456789abcdef" ); - // The declared length covers the committed `client_secret` too: it is - // the body GitHub parsed, not the part of it this side can read. - bytes memory whole = abi.encodePacked(_exchangeHead(prefix.length + 40), prefix); - uint32 wholeEnd = uint32(whole.length); - uint32 secretEnd = wholeEnd + 40; // `&client_secret=`, committed + } + /// One revealed run covering a request whole, with no commitment: the + /// shape `github/v1` fixes for its exchange. + function _wholeSent(bytes memory whole) private pure returns (AttestationBuilder.Direction memory) { return AttestationBuilder.Direction({ revealed: AttestationBuilder.one(AttestationBuilder.Range({start: 0, value: whole})), - commitments: AttestationBuilder.one( - AttestationBuilder.Commitment({start: wholeEnd, end: secretEnd, value: bytes32(uint256(0x5EC1E7))}) - ), - length: secretEnd + commitments: AttestationBuilder.none(), + length: uint32(whole.length) }); } + /// The exchange request as the profile fixes it: one revealed run over the + /// request whole. The head boundary sits inside that run, so the body is + /// located by the framing rather than by a range position. + function _exchangeSent() private view returns (AttestationBuilder.Direction memory) { + bytes memory body = _exchangeBody(); + return _wholeSent(abi.encodePacked(_exchangeHead(body.length), body)); + } + /// The exchange response: the bearer committed and framed by the revealed /// `"access_token":"` anchor and its closing quote, every other byte hidden /// behind a commitment of its own. @@ -514,9 +516,9 @@ contract GitHubPlatformVerifierTest is Test { proof: vm.parseJsonBytes(json, ".identity.notary_signature") }); ICeremony.VerifiedClaim memory f = this.run{value: quote}(s); - assertEq(f.userId, "18346821"); - assertEq(f.handle, "xgreenx"); - assertEq(string(f.clientIdentifier), "Ov23liIOfT7uQ9707Fpz"); + assertEq(f.userId, "293919812"); + assertEq(f.handle, "testyakly"); + assertEq(string(f.clientIdentifier), "Iv23lioEM9NAR9vO8CmT"); assertEq(f.sessionId, digest); } @@ -561,8 +563,9 @@ contract GitHubPlatformVerifierTest is Test { return ICeremony.Attestation({attestedData: attested, proof: _sign(attested)}); } - /// Commitment FIRST, then one revealed run: tiles, one commitment as the profile demands, - /// head boundary inside the run. Only TlsNotaryVerifierBase.sol:294 stands between this and acceptance. + /// A commitment over the request line, then one revealed run: the two + /// tile, so coverage passes and the origin rule is what refuses this -- + /// before the commitment count is ever looked at. function test_rejectsAnExchangeWhoseRequestLineIsHidden() public { bytes memory prefix = abi.encodePacked( "client_id=Iv1.8a61f9b3a7aba766&code=abc&redirect_uri=https%3A%2F%2Fa.example&code_verifier=", @@ -583,6 +586,28 @@ contract GitHubPlatformVerifierTest is Test { this.run{value: quote}(s); } + /// @dev The exchange layout itself, asserted against the verifier rather + /// than against the generated constant: a request that hides a body + /// suffix behind a commitment is refused, however well formed the + /// rest of it is. + function test_rejectsAnExchangeThatCommitsABodySuffix() public { + bytes memory body = _exchangeBody(); + bytes memory whole = abi.encodePacked(_exchangeHead(body.length + 8), body); + uint32 wholeEnd = uint32(whole.length); + AttestationBuilder.Direction memory sent = AttestationBuilder.Direction({ + revealed: AttestationBuilder.one(AttestationBuilder.Range({start: 0, value: whole})), + commitments: AttestationBuilder.one( + AttestationBuilder.Commitment({start: wholeEnd, end: wholeEnd + 8, value: bytes32(uint256(0x5EC1E7))}) + ), + length: wholeEnd + 8 + }); + bytes memory a = AttestationBuilder.encode(CeremonyProfile.AUTHORITY_GITHUB, T0, sent, _exchangeResponse()); + TlsNotaryVerifierBase.TlsNotaryProof memory s = _payload(); + s.tokenSession = ICeremony.Attestation({attestedData: a, proof: _sign(a)}); + vm.expectRevert(abi.encodeWithSelector(TlsNotaryVerifierBase.WrongTokenRequestLayout.selector, 1, 1)); + this.run{value: quote}(s); + } + /// @dev REQ-COMMON-21B, on the profile whose exchange this repository does /// not compose: `github/v1` pins its OWN head, so a service sending a /// request X's constant would have accepted is still refused here. @@ -590,24 +615,12 @@ contract GitHubPlatformVerifierTest is Test { /// what decides whether GitHub reads the bytes `formField` reads as a /// form at all. function test_rejectsAnotherMediaTypeOnTheExchange() public { - bytes memory prefix = abi.encodePacked( - "client_id=Iv1.8a61f9b3a7aba766&code=abc&redirect_uri=https%3A%2F%2Fa.example&code_verifier=", - CeremonyAuthorization.codeVerifier(digest, AUTH_NONCE) - ); + bytes memory body = _exchangeBody(); bytes memory head = _exchangeHead( "host: github.com\r\ncontent-type: application/json\r\naccept: application/json\r\nconnection: close\r\n", - prefix.length + 40 + body.length ); - bytes memory whole = abi.encodePacked(head, prefix); - AttestationBuilder.Direction memory sent = AttestationBuilder.Direction({ - revealed: AttestationBuilder.one(AttestationBuilder.Range({start: 0, value: whole})), - commitments: AttestationBuilder.one( - AttestationBuilder.Commitment({ - start: uint32(whole.length), end: uint32(whole.length) + 40, value: bytes32(uint256(0x5EC1E7)) - }) - ), - length: uint32(whole.length) + 40 - }); + AttestationBuilder.Direction memory sent = _wholeSent(abi.encodePacked(head, body)); bytes memory a = AttestationBuilder.encode(CeremonyProfile.AUTHORITY_GITHUB, T0, sent, _exchangeResponse()); TlsNotaryVerifierBase.TlsNotaryProof memory s = _payload(); s.tokenSession = ICeremony.Attestation({attestedData: a, proof: _sign(a)}); @@ -619,25 +632,13 @@ contract GitHubPlatformVerifierTest is Test { /// Service's own business -- a `user-agent`, say -- as long as it is /// not one of the forbidden names. The exchange still verifies. function test_acceptsAnUnlistedHeaderOnTheExchange() public { - bytes memory prefix = abi.encodePacked( - "client_id=Iv1.8a61f9b3a7aba766&code=abc&redirect_uri=https%3A%2F%2Fa.example&code_verifier=", - CeremonyAuthorization.codeVerifier(digest, AUTH_NONCE) - ); + bytes memory body = _exchangeBody(); bytes memory head = _exchangeHead( "host: github.com\r\nuser-agent: libid-bridge/0.3.0\r\ncontent-type: application/x-www-form-urlencoded\r\n" "accept: application/json\r\nconnection: close\r\n", - prefix.length + 40 + body.length ); - bytes memory whole = abi.encodePacked(head, prefix); - AttestationBuilder.Direction memory sent = AttestationBuilder.Direction({ - revealed: AttestationBuilder.one(AttestationBuilder.Range({start: 0, value: whole})), - commitments: AttestationBuilder.one( - AttestationBuilder.Commitment({ - start: uint32(whole.length), end: uint32(whole.length) + 40, value: bytes32(uint256(0x5EC1E7)) - }) - ), - length: uint32(whole.length) + 40 - }); + AttestationBuilder.Direction memory sent = _wholeSent(abi.encodePacked(head, body)); bytes memory a = AttestationBuilder.encode(CeremonyProfile.AUTHORITY_GITHUB, T0, sent, _exchangeResponse()); TlsNotaryVerifierBase.TlsNotaryProof memory s = _payload(); s.tokenSession = ICeremony.Attestation({attestedData: a, proof: _sign(a)}); diff --git a/solidity/contracts/ceremony/test/fixtures/github-ceremony-real.json b/solidity/contracts/ceremony/test/fixtures/github-ceremony-real.json index c93c89a0..be8e575d 100644 --- a/solidity/contracts/ceremony/test/fixtures/github-ceremony-real.json +++ b/solidity/contracts/ceremony/test/fixtures/github-ceremony-real.json @@ -1,27 +1,27 @@ { "authorization_digest": "0x09951c39dc62c5e74abc8c8607ec5c01b78e8d8bf6ff6da82f4cecbe72ace31f", "authorization_nonce": "0x5555555555555555555555555555555555555555555555555555555555555555", - "captured_at": 1789084207, + "captured_at": 1789657072, "ceremony_version": 1, "chain_id": 31337, "code_verifier": "NaJ86Rvwho11eSM3UDVCvhXt8Ykz9mWqpo2EVki2PH0", "identity": { - "attested_data": "0xa5d9c1d593bc385a23a2d56116aab1951e3c66296476c7a7396a515105e8b2c1000000006aa3422f0000014900000c710000000000000002000000000000000000000040474554202f7573657220485454502f312e310d0a686f73743a206170692e6769746875622e636f6d0d0a617574686f72697a6174696f6e3a20426561726572200000006800000000000000e10d0a6163636570743a206170706c69636174696f6e2f766e642e6769746875622b6a736f6e0d0a757365722d6167656e743a204d6f7a696c6c612f352e3020284d6163696e746f73683b20496e74656c204d6163204f5320582031305f31355f3729204170706c655765624b69742f3533372e333620284b48544d4c2c206c696b65204765636b6f29204368726f6d652f3132382e302e302e30205361666172692f3533372e33360d0a782d6769746875622d6170692d76657273696f6e3a20323032322d31312d32380d0a636f6e6e656374696f6e3a20636c6f73650d0a0d0a000000000000000100000040000000684a8b915bb6394ab6c2bfba66f54089a17e90eed92df06160e9796c24504a84130000000000000002000005b30000000000000012226c6f67696e223a202278677265656e7822000005c9000000000000000f226964223a2031383334363832312c000000000000000300000000000005b35183621029bef8d9d52e91675ec58ecf770847f2e499edfd905c55d3cd65f0bd000005c5000005c9d820f020ce766b8ba0e815ffb641af76a82c7fd66731482284a43b8aee093ca6000005d800000c71af235a0c9e39a1d376b0587288d38baebee103983763f194fbf81083912dc201", + "attested_data": "0xa5d9c1d593bc385a23a2d56116aab1951e3c66296476c7a7396a515105e8b2c1000000006aabfff00000014900000b770000000000000002000000000000000000000040474554202f7573657220485454502f312e310d0a686f73743a206170692e6769746875622e636f6d0d0a617574686f72697a6174696f6e3a20426561726572200000006800000000000000e10d0a6163636570743a206170706c69636174696f6e2f766e642e6769746875622b6a736f6e0d0a757365722d6167656e743a204d6f7a696c6c612f352e3020284d6163696e746f73683b20496e74656c204d6163204f5320582031305f31355f3729204170706c655765624b69742f3533372e333620284b48544d4c2c206c696b65204765636b6f29204368726f6d652f3132382e302e302e30205361666172692f3533372e33360d0a782d6769746875622d6170692d76657273696f6e3a20323032322d31312d32380d0a636f6e6e656374696f6e3a20636c6f73650d0a0d0a000000000000000100000040000000686ceadbed508dd07aff0789c145fd3d95e677fa44b2647aabe2599c20913367db0000000000000002000006310000000000000014226c6f67696e223a20227465737479616b6c7922000006490000000000000010226964223a203239333931393831322c0000000000000003000000000000063196a24541d69b549beeaa08dcbffbcb56aa028619098a2cbc5de20820071f8bc30000064500000649a27b4f47b7a9e2443d084c6de4063c5c69b17cc47e182eaf85ddb233a2bf96540000065900000b77a86ec4c9f4198203a5c32e6b8055023b7fc8901fef060e45b6d977eea2c6da23", "authority": "api.github.com", - "created_at": 1789084207, + "created_at": 1789657072, "endpoint": "https://api.github.com/user", - "notary_signature": "0x6177d3bedb32a33c5396ead197a7cf2f787636b55143e8a722e2237975be4a232d0cc48b58cc6fd710f1f365f72a5073d6c369934151dc315cccebdcc9a9f79e1b" + "notary_signature": "0xd3d5bddf9008cd9be7b8ff51db584c3308d8c006366e0adef2ce8169b9629b16642ad918c6fb64bc5a4d8ad6896127069be32191cfc9223d7a3992805a9536001b" }, "notary": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", "operation_domain": "0xcb29bed0428519ef88a3d670e8203db76e06f41aca3e684e2c63b516c9b93e1b", "platform": "github", "source": "captured: a real MPC-TLS session against the platform, the verifier in-process, by libid-rs examples/capture_ceremony.rs", "token": { - "attested_data": "0x06785da520052bf40d5bf506fb493c41162f55d4e17dffa8b21f02598e981533000000006aa342050000019f000012410000000000000001000000000000000000000168504f5354202f6c6f67696e2f6f617574682f6163636573735f746f6b656e20485454502f312e310d0a686f73743a206769746875622e636f6d0d0a636f6e74656e742d747970653a206170706c69636174696f6e2f782d7777772d666f726d2d75726c656e636f6465640d0a6163636570743a206170706c69636174696f6e2f6a736f6e0d0a636f6e6e656374696f6e3a20636c6f73650d0a636f6e74656e742d6c656e6774683a203233390d0a0d0a636c69656e745f69643d4f7632336c69494f66543775513937303746707a26636f64653d63383434383335663636316164633930616266362672656469726563745f7572693d687474702533412532462532463132372e302e302e31253341383732322532466175746825324667697468756225324663616c6c6261636b26636f64655f76657269666965723d4e614a3836527677686f313165534d33554456437668587438596b7a396d5771706f3245566b69325048300000000000000001000001680000019f3c3c7c45453cdc6951880d71bc1928371f2074323b3d45f1b2937c775c9e767f0000000000000002000011d60000000000000010226163636573735f746f6b656e223a220000120e000000000000000122000000000000000300000000000011d6b4c1eeaa38e297c51ce58609d94b3e447c88ad940fbb782b63fa78e22a8b86ed000011e60000120e672ce8194b6527adae0462be6adb1693612c6d714a5ae2ea35d5b60a5435885c0000120f00001241e028481b94f4c033d88de66386fd9b4cefca86568ba9d09dd7f3af9f8918e1e9", + "attested_data": "0x06785da520052bf40d5bf506fb493c41162f55d4e17dffa8b21f02598e981533000000006aabffdb00000196000012ec0000000000000001000000000000000000000196504f5354202f6c6f67696e2f6f617574682f6163636573735f746f6b656e20485454502f312e310d0a686f73743a206769746875622e636f6d0d0a636f6e74656e742d747970653a206170706c69636174696f6e2f782d7777772d666f726d2d75726c656e636f6465640d0a6163636570743a206170706c69636174696f6e2f6a736f6e0d0a636f6e6e656374696f6e3a20636c6f73650d0a636f6e74656e742d6c656e6774683a203233300d0a0d0a636c69656e745f69643d497632336c696f454d394e415239764f38436d5426636f64653d34646339313832643066623031313433366435612672656469726563745f7572693d687474702533412532462532463132372e302e302e31253341383732322532466175746825324663616c6c6261636b26636f64655f76657269666965723d4e614a3836527677686f313165534d33554456437668587438596b7a396d5771706f3245566b693250483026636c69656e745f7365637265743d3430666665353235303137363139623337306637306537353564653139633738373739313565383600000000000000000000000000000002000011f00000000000000010226163636573735f746f6b656e223a2200001228000000000000000122000000000000000300000000000011f0e8ca400a1e9696eccb427f8f2642adb14947aa9acedea44101addaae631b54b6000012000000122840b353ca528c35c8a82c80ace2f96d195a87be1d09f5fded20238a551183768d00001229000012ecdc2293498f444a554c2757fb2a89c31b5ed22afbaed3274e5ad3d2801e5cc148", "authority": "github.com", - "created_at": 1789084165, + "created_at": 1789657051, "endpoint": "https://github.com/login/oauth/access_token", - "notary_signature": "0xe25b86e34c0c354dab242be5c80af5f31886002077135a69511655dad048fda672ce3c2562f9b466c48ccada92dd4c21accd9ef1c3cdd79a682c4c12ed326f891b" + "notary_signature": "0x2ebee6ee6912b495c07d1f527e03119681fe12c09b0783805b4fd73c5e89f9d54b3ef6638fa31024fea276ef3b343dc4d874febbeb1d5cc86b0bd96d4e0eb95b1b" }, "transaction_data": "0x000000000000000000000000000000000000000000000000000000000000beef" } diff --git a/solidity/contracts/ceremony/test/fixtures/github-ceremony-session.json b/solidity/contracts/ceremony/test/fixtures/github-ceremony-session.json index 5ae23b84..12cb835e 100644 --- a/solidity/contracts/ceremony/test/fixtures/github-ceremony-session.json +++ b/solidity/contracts/ceremony/test/fixtures/github-ceremony-session.json @@ -59,22 +59,12 @@ "operation_domain": "0xcb29bed0428519ef88a3d670e8203db76e06f41aca3e684e2c63b516c9b93e1b", "platform": "github", "token": { - "attested_data": "0x06785da520052bf40d5bf506fb493c41162f55d4e17dffa8b21f02598e9815330000000069800e800000017f000000c30000000000000001000000000000000000000140504f5354202f6c6f67696e2f6f617574682f6163636573735f746f6b656e20485454502f312e310d0a686f73743a206769746875622e636f6d0d0a636f6e74656e742d747970653a206170706c69636174696f6e2f782d7777772d666f726d2d75726c656e636f6465640d0a6163636570743a206170706c69636174696f6e2f6a736f6e0d0a636f6e6e656374696f6e3a20636c6f73650d0a636f6e74656e742d6c656e6774683a203230370d0a0d0a636c69656e745f69643d4976312e3861363166396233613761626137363626636f64653d6162633132332672656469726563745f7572693d68747470732533412532462532466170702e6578616d706c65253246636226636f64655f76657269666965723d4e614a3836527677686f313165534d33554456437668587438596b7a396d5771706f3245566b69325048300000000000000001000001400000017f20fc3ee20a3c9a01ba2fe4d83ee3d79aef04568ad7904faebf3197eb6954efdb00000000000000020000006a0000000000000010226163636573735f746f6b656e223a22000000a00000000000000001220000000000000003000000000000006a7e321e6797d9677799af03b5066aa04bf19ed953750f3ac525c42b984d34777f0000007a000000a0b03a9a05bacef13ee707781f408db6742e38a5ec05ebf4244a6237c710ae986f000000a1000000c3e651e1d0e45b96591c33265d677a2033572834836de79568b908170bf5a28dc8", + "attested_data": "0x06785da520052bf40d5bf506fb493c41162f55d4e17dffa8b21f02598e9815330000000069800e800000017f000000c3000000000000000100000000000000000000017f504f5354202f6c6f67696e2f6f617574682f6163636573735f746f6b656e20485454502f312e310d0a686f73743a206769746875622e636f6d0d0a636f6e74656e742d747970653a206170706c69636174696f6e2f782d7777772d666f726d2d75726c656e636f6465640d0a6163636570743a206170706c69636174696f6e2f6a736f6e0d0a636f6e6e656374696f6e3a20636c6f73650d0a636f6e74656e742d6c656e6774683a203230370d0a0d0a636c69656e745f69643d4976312e3861363166396233613761626137363626636f64653d6162633132332672656469726563745f7572693d68747470732533412532462532466170702e6578616d706c65253246636226636f64655f76657269666965723d4e614a3836527677686f313165534d33554456437668587438596b7a396d5771706f3245566b693250483026636c69656e745f7365637265743d303132333435363738396162636465663031323334353637383961626364656630313233343536373839616263646566000000000000000000000000000000020000006a0000000000000010226163636573735f746f6b656e223a22000000a00000000000000001220000000000000003000000000000006a69ef2b9767091c2e7474d55f22b3891053d487f75010f3cc4229d8cc174c3ad20000007a000000a06c698116c9abf9ab89846e5ee03bf56acafb76180010ffd52791e52e26c384f6000000a1000000c3a9c474140fd209c2808e8fe0c58c65dbc3ccd2e672a1db595b2565e84672f9fd", "endpoint": "https://github.com/login/oauth/access_token", - "notary_signature": "0x22a0630c0b94db766cdf35de49558f96d009f7a3f6d72cffe6533cf2611ef1be0ed3781fb25c745ac5dbeef3d493d1e68d3e5173f7585466b094de2dcde835561c", + "notary_signature": "0xf34f6f6de1d2daec424ff0fb2b89a6afeb43f9737a226c1a11bf0c8fb75146a9415cc33046b0757aba2a371ee58087067115280cffc17be1fed3fcc3593b0db71b", "openings": [ { "blinder": "0xf2bea951f6b30b9ac6d20b9b5fc8d897", - "direction": "sent", - "ranges": [ - [ - 320, - 383 - ] - ] - }, - { - "blinder": "0xe906469f340e60279fee288dfa21d72c", "direction": "received", "ranges": [ [ @@ -84,7 +74,7 @@ ] }, { - "blinder": "0x4e9159aa47b770b706d1b66d61435d7a", + "blinder": "0xe906469f340e60279fee288dfa21d72c", "direction": "received", "ranges": [ [ @@ -94,7 +84,7 @@ ] }, { - "blinder": "0x7973e68ddcaa8649646a049a405fab62", + "blinder": "0x4e9159aa47b770b706d1b66d61435d7a", "direction": "received", "ranges": [ [ diff --git a/ts/packages/contracts/src/ceremony/profiles.ts b/ts/packages/contracts/src/ceremony/profiles.ts index 2c7633ba..69683d24 100644 --- a/ts/packages/contracts/src/ceremony/profiles.ts +++ b/ts/packages/contracts/src/ceremony/profiles.ts @@ -106,10 +106,10 @@ export const X: Profile = { } /** - * A confidential client. The exchange runs in the deployment because the - * secret cannot reach the browser, and the two sessions are served by - * DIFFERENT hosts -- which is why one pinned authority per profile would - * be wrong. + * The credential GitHub calls `client_secret` is sent and revealed, so + * an attestation publishes it and nothing here is confidential. The two + * sessions are served by DIFFERENT hosts -- which is why one pinned + * authority per profile would be wrong. */ export const GITHUB: Profile = { platform: 'github', @@ -121,7 +121,7 @@ export const GITHUB: Profile = { path: '/login/oauth/access_token', requestLine: 'POST /login/oauth/access_token ', }, - secretField: 'client_secret', + secretField: null, requiredHeaders: ['host: github.com', 'content-type: application/x-www-form-urlencoded'], }, identity: {