Skip to content
Merged
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
10 changes: 5 additions & 5 deletions rust/profiles/src/profiles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion rust/profiles/tests/wire.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
17 changes: 7 additions & 10 deletions solidity/contracts/ceremony/CeremonyAttestation.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion solidity/contracts/ceremony/CeremonyProfile.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
61 changes: 26 additions & 35 deletions solidity/contracts/ceremony/GitHubPlatformVerifier.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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() {
Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
}
Expand Down
19 changes: 10 additions & 9 deletions solidity/contracts/ceremony/TlsNotaryVerifierBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down
27 changes: 15 additions & 12 deletions solidity/contracts/ceremony/profiles.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
Loading
Loading