From 23b04198d90b884c62f1f517bc1d554cd502adfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Zochniak?= Date: Wed, 17 Jun 2026 17:48:02 +0200 Subject: [PATCH] Move reddit PVL to HTML --- go/pvl/kit_test.go | 10 +- go/pvl/testdata/reddit-max.html | 88 ++++++++++++++++ go/pvl/testdata/reddit-max.xml | 1 - go/pvl/testdata/reddit-terribletext5299.html | 31 ++++++ go/pvl/testdata/reddit-terribletext5299.xml | 1 - pvl-tools/kit.json | 2 +- pvl-tools/tab/1.cson | 103 +++++++------------ 7 files changed, 165 insertions(+), 71 deletions(-) create mode 100644 go/pvl/testdata/reddit-max.html delete mode 100644 go/pvl/testdata/reddit-max.xml create mode 100644 go/pvl/testdata/reddit-terribletext5299.html delete mode 100644 go/pvl/testdata/reddit-terribletext5299.xml diff --git a/go/pvl/kit_test.go b/go/pvl/kit_test.go index 2d082857b249..54211636e6da 100644 --- a/go/pvl/kit_test.go +++ b/go/pvl/kit_test.go @@ -99,10 +99,10 @@ func TestKitRedditMax(t *testing.T) { testRemoteUsername: "maxtaco", // remote (service) username armoredSig: string(armoredSig), - testResponseFile: "testdata/reddit-max.xml", + testResponseFile: "testdata/reddit-max.html", testAPIURL: "https://www.reddit.com/r/KeybaseProofs/comments/2clf9c/my_keybase_proof_redditmaxtaco_keybasemax/.json", - urloverride: "https://old.reddit.com/r/KeybaseProofs/comments/2clf9c/my_keybase_proof_redditmaxtaco_keybasemax/.rss", + urloverride: "https://old.reddit.com/r/KeybaseProofs/comments/2clf9c/my_keybase_proof_redditmaxtaco_keybasemax/", shouldwork: true, } @@ -110,7 +110,7 @@ func TestKitRedditMax(t *testing.T) { // Try a response for a different proof, should not work utBad := ut - utBad.testResponseFile = "testdata/reddit-terribletext5299.xml" + utBad.testResponseFile = "testdata/reddit-terribletext5299.html" utBad.shouldwork = false utBad.errstatus = keybase1.ProofStatus_BAD_USERNAME runKitUnitTest(t, &utBad) @@ -126,10 +126,10 @@ func TestKitRedditTerribleText5299(t *testing.T) { testRemoteUsername: "terrible-text5299", // remote (service) username armoredSig: armoredSig, - testResponseFile: "testdata/reddit-terribletext5299.xml", + testResponseFile: "testdata/reddit-terribletext5299.html", testAPIURL: "https://www.reddit.com/r/KeybaseProofs/comments/1pqpxtp/my_keybase_proof_redditterribletext5299/.json", - urloverride: "https://old.reddit.com/r/KeybaseProofs/comments/1pqpxtp/my_keybase_proof_redditterribletext5299/.rss", + urloverride: "https://old.reddit.com/r/KeybaseProofs/comments/1pqpxtp/my_keybase_proof_redditterribletext5299/", shouldwork: true, } diff --git a/go/pvl/testdata/reddit-max.html b/go/pvl/testdata/reddit-max.html new file mode 100644 index 000000000000..0ae6b1355103 --- /dev/null +++ b/go/pvl/testdata/reddit-max.html @@ -0,0 +1,88 @@ +My Keybase proof [reddit:maxtaco = keybase:max] (CV2H0VPmWQwtbAMnUtPENIWh9k5huZ1Y-Yn22iiyqDI) : KeybaseProofs
this post was submitted on
9 points (100% upvoted)

KeybaseProofs


+ +

Keybase proofs!

+ +

If you're a reddit user and a keybase user, then this is the subreddit to cryptographically tie the two accounts together. To generate your proof, just follow the instructions on your profile page on Keybase.io. Or, using the command line, run keybase prove reddit.

+ +
+ +

If you'd like to get the public key of any reddit user, just type their reddit username in the search box at Keybase.io, and you'll get it.

+ +

If you'd like to discuss Keybase in general, visit /r/Keybase, the general Keybase subreddit. This subreddit is for Keybase proofs only.

+ +
+
+
a community for
×
no comments (yet)

there doesn't seem to be anything here

π Rendered by PID 2855211 on reddit-service-r2-loggedout-5c5695bf4b-jtjh2 at 2026-06-17 15:11:48.744885+00:00 running 3184619 country code: US.

\ No newline at end of file diff --git a/go/pvl/testdata/reddit-max.xml b/go/pvl/testdata/reddit-max.xml deleted file mode 100644 index 6c1f2d599e5a..000000000000 --- a/go/pvl/testdata/reddit-max.xml +++ /dev/null @@ -1 +0,0 @@ -2026-06-03T15:35:10+00:00https://www.redditstatic.com/icon.png//r/KeybaseProofs/comments/2clf9c/my_keybase_proof_redditmaxtaco_keybasemax/.rsshttps://b.thumbs.redditmedia.com/14PXE-Ui9Gz3ACLrlRmR9d8y0kicn63LpvqviwqDVzQ.pngTo cryptographically tie your reddit account to a PGP public key, you can generate a signed statement with Keybase.io, and post it here as a text post. This subreddit is strictly for such announcements.My Keybase proof [reddit:maxtaco = keybase:max] (CV2H0VPmWQwtbAMnUtPENIWh9k5huZ1Y-Yn22iiyqDI) : KeybaseProofs/u/maxtacohttps://www.reddit.com/user/maxtaco<!-- SC_OFF --><div class="md"><h3>Keybase proof</h3> <p>I hereby claim:</p> <ul> <li>I am <a href="https://www.reddit.com/user/maxtaco">maxtaco</a> on reddit.</li> <li>I am <a href="https://keybase.io/max">max</a> on keybase.</li> <li>I have a public key whose fingerprint is 8EFB E2E4 DD56 B352 7363 4E8F 6052 B2AD 31A6 631C</li> </ul> <p>To claim this, I am signing this object:</p> <pre><code>{ &quot;body&quot;: { &quot;key&quot;: { &quot;fingerprint&quot;: &quot;8efbe2e4dd56b35273634e8f6052b2ad31a6631c&quot;, &quot;host&quot;: &quot;keybase.io&quot;, &quot;key_id&quot;: &quot;6052b2ad31a6631c&quot;, &quot;uid&quot;: &quot;dbb165b7879fe7b1174df73bed0b9500&quot;, &quot;username&quot;: &quot;max&quot; }, &quot;service&quot;: { &quot;name&quot;: &quot;reddit&quot;, &quot;username&quot;: &quot;maxtaco&quot; }, &quot;type&quot;: &quot;web_service_binding&quot;, &quot;version&quot;: 1 }, &quot;ctime&quot;: 1407166100, &quot;expire_in&quot;: 157680000, &quot;prev&quot;: &quot;b328cdb4904ebcc196a49c67355f3ee9239bd735d7246e90cf20c7e7307e706b&quot;, &quot;seqno&quot;: 55, &quot;tag&quot;: &quot;signature&quot; } </code></pre> <p>with the PGP key whose fingerprint is <a href="https://keybase.io/max">8EFB E2E4 DD56 B352 7363 4E8F 6052 B2AD 31A6 631C</a> (captured above as <code>body.key.fingerprint</code>), yielding the PGP signature:</p> <pre><code>-----BEGIN PGP MESSAGE----- Version: Keybase OpenPGP v0.2.0 Comment: https://keybase.io/crypto yMHQAnicbZFdSBRRFMd3/Uhc28gMDRGs20MPWc7nnZmF8MUsRFqoQANhnTv37jar O7PNruuuplQEYh+apCUWIi2FgfosRWX2BUZRPdgXaRIL4rZIIpKB1ozYU73cy/mf 3//POZynznSbw97nKHPa1zJm7C/HE422YzN3eloA0nEMuFpAPVn/vKrmI0bQULUw cAGReBFhCIcxDxHLMwILWY6IXkjxDGJkzNIyhCytgBJwUg9ZDjMGySGyX9VNzSw8 KjbV//CN6w2MEA15JIiC5CUCommBw16BRQRTSOIpygJDxNDkADHpgBwFrSXAFCKq QqxxNxoGwVgN/wOHZUW3DOFY0FKaCPJseD1I1bC5qmmJECOk6hpw0SaphFXLTHOU QENIU1QJINGgahCPahG8AEWKstSgQSJmJGIZUcGIkyiOIEWhJShzkgIFlue9LCES w0oImxUWGA4SiVK8DKUIRGAp86EgAtY2pzQduHjenFP2mZkh1afJ4UaDgNaJ8doM m91h25SZZl3L5sje+veG8StZqwWdmwd7H6bldG87PlfUnbvKn60aXRQPNFWn9qaW d4uvqPmRe7Gq1I4fswVJd27zKjswlSzKmW6YK8bx6smcn+nZDzL7XE3P7kfhbVi4 53JbQh/J2DLddW7X8/yk80Mt6AxkDT6KtM1Ime2+oYXAXGlsSRgbKK5Tot/Lhw72 FehL+bqbgdrUkcTH+LXhio41eTLlvtWf97nm7aGvv0sP+5tfLyfjee+r57dfcDu/ NRhxT3vlyonk43f7rrt9QgqJvy4VVu5URv1d/S+Gy598uXoR1Hb1DvvuVpzuqHPb 3kzUNLAriZ5ix2J4diF+dM0fLKz5VH/z/Bn/jbLxP52YD5k= =5gCl -----END PGP MESSAGE----- </code></pre> <p>And finally, I am proving ownership of the reddit account by posting this on the subreddit <a href="https://www.reddit.com/r/KeybaseProofs">KeybaseProofs</a>.</p> <h3>My publicly-auditable identity:</h3> <p><a href="https://keybase.io/max">https://keybase.io/max</a></p> <h3>To join me:</h3> <p>After a day of posting this and completing the proof, I&#39;ll be granted invitations to Keybase. Let me know if you would like access to the alpha.</p> </div><!-- SC_ON --> &#32; submitted by &#32; <a href="https://www.reddit.com/user/maxtaco"> /u/maxtaco </a> <br/> <span><a href="https://www.reddit.com/r/KeybaseProofs/comments/2clf9c/my_keybase_proof_redditmaxtaco_keybasemax/">[link]</a></span> &#32; <span><a href="https://www.reddit.com/r/KeybaseProofs/comments/2clf9c/my_keybase_proof_redditmaxtaco_keybasemax/">[comments]</a></span>t3_2clf9c2014-08-04T15:28:32+00:002014-08-04T15:28:32+00:00My Keybase proof [reddit:maxtaco = keybase:max] (CV2H0VPmWQwtbAMnUtPENIWh9k5huZ1Y-Yn22iiyqDI) \ No newline at end of file diff --git a/go/pvl/testdata/reddit-terribletext5299.html b/go/pvl/testdata/reddit-terribletext5299.html new file mode 100644 index 000000000000..1675abf5e442 --- /dev/null +++ b/go/pvl/testdata/reddit-terribletext5299.html @@ -0,0 +1,31 @@ +My Keybase proof [reddit:terrible-text5299 = keybase:testusera56b] (5dkY3c8zpYl3UUCsuzL7nL2WwC17qhXjubLnfqd_djw) : KeybaseProofs
this post was submitted on
1 point (100% upvoted)

KeybaseProofs


+ +

Keybase proofs!

+ +

If you're a reddit user and a keybase user, then this is the subreddit to cryptographically tie the two accounts together. To generate your proof, just follow the instructions on your profile page on Keybase.io. Or, using the command line, run keybase prove reddit.

+ +
+ +

If you'd like to get the public key of any reddit user, just type their reddit username in the search box at Keybase.io, and you'll get it.

+ +

If you'd like to discuss Keybase in general, visit /r/Keybase, the general Keybase subreddit. This subreddit is for Keybase proofs only.

+ +
+
+
a community for
×
all 1 comments

[–]Terrible-Text5299[S] 0 points1 point  (0 children)

I'm still here

+
+

π Rendered by PID 41 on reddit-service-r2-loggedout-5c5695bf4b-nnlvg at 2026-06-17 14:30:39.897654+00:00 running 3184619 country code: US.

\ No newline at end of file diff --git a/go/pvl/testdata/reddit-terribletext5299.xml b/go/pvl/testdata/reddit-terribletext5299.xml deleted file mode 100644 index ccab7b4df010..000000000000 --- a/go/pvl/testdata/reddit-terribletext5299.xml +++ /dev/null @@ -1 +0,0 @@ -2026-06-03T20:07:26+00:00https://www.redditstatic.com/icon.png//r/KeybaseProofs/comments/1pqpxtp/my_keybase_proof_redditterribletext5299/.rsshttps://b.thumbs.redditmedia.com/14PXE-Ui9Gz3ACLrlRmR9d8y0kicn63LpvqviwqDVzQ.pngTo cryptographically tie your reddit account to a PGP public key, you can generate a signed statement with Keybase.io, and post it here as a text post. This subreddit is strictly for such announcements.My Keybase proof [reddit:terrible-text5299 = keybase:testusera56b] (5dkY3c8zpYl3UUCsuzL7nL2WwC17qhXjubLnfqd_djw) : KeybaseProofs/u/Terrible-Text5299https://old.reddit.com/user/Terrible-Text5299<!-- SC_OFF --><div class="md"><h3>Keybase proof</h3> <p>I am:</p> <ul> <li><a href="https://www.reddit.com/user/terrible-text5299">terrible-text5299</a> on reddit.</li> <li><a href="https://keybase.io/testusera56b">testusera56b</a> on keybase.</li> </ul> <p>Proof:</p> <pre><code>hKRib2R5hqhkZXRhY2hlZMOpaGFzaF90eXBlCqNrZXnEIwEgHgWHLK3x6NX4ksIQa0MQTKQtcnfRA3y+ztKIeclfnrkKp3BheWxvYWTESpcCBsQgQDVJotXMTHnH+HKa99mLrjMs68oq49uoBXhqeGIz/7LEICIPqp3niErOJO+Am4fBOcCo5f/l2DZ3qN4A+Ms3yMt6AgHCo3NpZ8RAwdsXmhWjKOfqIzqIgE/sMwilohfhWVSgCeBROZ4PWeXHsU5IHkTtY6R3hgu53cpNAKagk+Z5RlnjbXHTN5LOBahzaWdfdHlwZSCkaGFzaIKkdHlwZQildmFsdWXEIK08UoW2b90fZEYMrl1gopc2ug66tAclRuXWPhTomZ1bo3RhZ80CAqd2ZXJzaW9uAQ== </code></pre> </div><!-- SC_ON --> &#32; submitted by &#32; <a href="https://old.reddit.com/user/Terrible-Text5299"> /u/Terrible-Text5299 </a> <br/> <span><a href="https://old.reddit.com/r/KeybaseProofs/comments/1pqpxtp/my_keybase_proof_redditterribletext5299/">[link]</a></span> &#32; <span><a href="https://old.reddit.com/r/KeybaseProofs/comments/1pqpxtp/my_keybase_proof_redditterribletext5299/">[comments]</a></span>t3_1pqpxtp2025-12-19T16:47:34+00:002025-12-19T16:47:34+00:00My Keybase proof [reddit:terrible-text5299 = keybase:testusera56b] (5dkY3c8zpYl3UUCsuzL7nL2WwC17qhXjubLnfqd_djw)/u/Terrible-Text5299https://old.reddit.com/user/Terrible-Text5299<!-- SC_OFF --><div class="md"><p>I&#39;m still here</p> </div><!-- SC_ON -->t1_opkvz3h2026-06-03T20:03:29+00:00/u/Terrible-Text5299 on My Keybase proof [reddit:terrible-text5299 = keybase:testusera56b] (5dkY3c8zpYl3UUCsuzL7nL2WwC17qhXjubLnfqd_djw) \ No newline at end of file diff --git a/pvl-tools/kit.json b/pvl-tools/kit.json index 217551e63721..9380a59b3df6 100644 --- a/pvl-tools/kit.json +++ b/pvl-tools/kit.json @@ -1 +1 @@ -{"kit_version":1,"ctime":1780520080,"tab":{"1":{"pvl_version":1,"revision":1,"services":{"coinbase":[[{"fill":{"with":"x","into":"tmp1"}},{"assert_regex_match":{"pattern":"^y$","from":"tmp1","error":["SERVICE_DEAD","coinbase proofs are no longer supported"]}}]],"dns":[[{"assert_regex_match":{"pattern":"^keybase-site-verification=%{sig_id_medium}$","from":"txt","error":["NOT_FOUND","matching DNS entry not found"]}}]],"facebook":[[{"assert_regex_match":{"pattern":"^[a-zA-Z0-9\\.]+$","from":"username_service","error":["BAD_USERNAME","Invalid characters in username '%{username_service}'"]}},{"regex_capture":{"pattern":"^https://(m|www)\\.facebook\\.com/([^/]*)/posts/([0-9]+)$","from":"hint_url","into":["unused1","username_from_url","post_id"],"error":["BAD_API_URL","Bad hint from server; URL should start with 'https://m.facebook.com/%{username_service}/posts/', got '%{hint_url}'"]}},{"assert_compare":{"cmp":"stripdots-then-cicmp","a":"username_from_url","b":"username_service","error":["BAD_API_URL","Bad hint from server; username in URL should match '%{username_service}', received '%{username_from_url}'"]}},{"fill":{"with":"https://m.facebook.com/%{username_from_url}/posts/%{post_id}","into":"our_url"}},{"fetch":{"kind":"html","from":"our_url"}},{"selector_css":{"selectors":["#m_story_permalink_view",0,"h3",1],"into":"post_text","error":["FAILED_PARSE","Could not find post text in Facebook's response"]}},{"whitespace_normalize":{"from":"post_text","into":"post_text_nw"}},{"regex_capture":{"pattern":"^Verifying myself: I am (\\S+) on Keybase.io. (\\S+)$","from":"post_text_nw","into":["username_from_post","sig_from_post"],"error":["TEXT_NOT_FOUND","Could not find Verifying myself: I am %{username_keybase} on Keybase.io. (%{sig_id_medium})"]}},{"assert_compare":{"cmp":"cicmp","a":"username_from_post","b":"username_keybase","error":["BAD_USERNAME","Wrong keybase username in post '%{username_from_post}' should be '%{username_keybase}'"]}},{"assert_compare":{"cmp":"exact","a":"sig_id_medium","b":"sig_from_post","error":["BAD_SIGNATURE","Could not find sig; '%{sig_from_post}' != '%{sig_id_medium}'"]}},{"selector_css":{"selectors":["#mobile_login_bar",0,"a",0],"attr":"href","into":"join_href","error":["CONTENT_FAILURE","Could not find 'Join' link"]}},{"regex_capture":{"pattern":"^/r\\.php\\?next=https.*facebook.com%2F([^/]*)%2Fposts.*$","from":"join_href","into":["username_from_join_href"],"error":["FAILED_PARSE","Could not interpret 'Join' link"]}},{"assert_compare":{"cmp":"stripdots-then-cicmp","a":"username_from_join_href","b":"username_service","error":["CONTENT_FAILURE","Bad hint from server; username in URL should match '%{username_service}', received '%{username_from_url}'"]}}]],"github":[[{"regex_capture":{"pattern":"^https://gist\\.github(?:usercontent)?\\.com/([^/]*)/.*$","from":"hint_url","into":["username_from_url"],"error":["BAD_API_URL","Bad hint from server; URL should start with either https://gist.github.com OR https://gist.githubusercontent.com"]}},{"assert_compare":{"cmp":"cicmp","a":"username_from_url","b":"username_service","error":["BAD_API_URL","Bad hint from server; URL should contain username matching %{username_service}; got %{username_from_url}"]}},{"fetch":{"kind":"string","from":"hint_url","into":"gist"}},{"assert_find_base64":{"needle":"sig","haystack":"gist"},"error":["TEXT_NOT_FOUND","Signature not found in body"]},{"whitespace_normalize":{"from":"gist","into":"gist_nw"}},{"assert_regex_match":{"pattern":"^((### Verifying myself: I am (https://keybase\\.io)?/%{username_keybase} As part of this verification process, I am signing this object and posting as a gist as github user \\*%{username_service}\\*)|(### Keybase proof I hereby claim: \\* I am %{username_service} on github\\. \\* I am %{username_keybase} \\(https://keybase\\.io/%{username_keybase}\\) on keybase\\.)) .*$","case_insensitive":true,"from":"gist_nw","error":["TEXT_NOT_FOUND","Found sig but gist begins with unexpected content"]}},{"assert_regex_match":{"pattern":"^.*!\\[.*$","negate":true,"case_insensitive":true,"from":"gist_nw","error":["CONTENT_FAILURE","Proof gist must not contain images"]}}]],"hackernews":[[{"regex_capture":{"pattern":"^https://hacker-news\\.firebaseio\\.com/v0/user/([^/]+)/about.json$","from":"hint_url","into":["username_from_url"],"error":["BAD_API_URL","Bad hint from server; URL should match https://hacker-news.firebaseio.com/v0/user/%{username_service}/about.json"]}},{"assert_compare":{"cmp":"cicmp","a":"username_from_url","b":"username_service","error":["BAD_API_URL","Bad hint from server; URL should contain username matching %{username_service}; got %{username_from_url}"]}},{"fetch":{"kind":"string","from":"hint_url","into":"profile"}},{"assert_regex_match":{"pattern":"^.*%{sig_id_medium}.*$","from":"profile","error":["TEXT_NOT_FOUND","Posted text does not include signature '%{sig_id_medium}'"]}}]],"reddit":[[{"regex_capture":{"pattern":"^https://www\\.reddit\\.com/r/([^/]+)/(.*)/(\\.json)$","from":"hint_url","into":["subreddit_from_url","path_remainder","json_trailer"],"error":["BAD_API_URL","URL should start with 'https://www.reddit.com/r/keybaseproofs'"]}},{"assert_regex_match":{"pattern":"^.*reddit.*$","from":"hint_url","error":["BAD_API_URL","URL should contain 'reddit'"]}},{"assert_regex_match":{"pattern":"^keybaseproofs$","case_insensitive":true,"from":"subreddit_from_url","error":["BAD_API_URL","URL contained wrong subreddit '%{subreddit_from_url}' !+ 'keybaseproofs'"]}},{"fill":{"with":"https://old.reddit.com/r/%{subreddit_from_url}/%{path_remainder}/.rss","into":"our_url"}},{"fetch":{"from":"our_url","kind":"html"}},{"selector_css":{"selectors":["feed","entry",0,"id",{"contents":true}],"into":"entry_id","error":["CONTENT_MISSING","Could not find entry ID"]}},{"assert_regex_match":{"pattern":"^t3_.+$","from":"entry_id","error":["CONTENT_FAILURE","Wanted an entry ID with 't3' prefix but got '%{entry_id}'"]}},{"selector_css":{"selectors":["feed","entry",0,"category"],"attr":"label","into":"subreddit_from_xml","error":["CONTENT_MISSING","Could not find category (subreddit)"]}},{"assert_regex_match":{"pattern":"^r/keybaseproofs$","case_insensitive":true,"from":"subreddit_from_xml","error":["CONTENT_FAILURE","Wrong subreddit %{subreddit_from_xml}"]}},{"selector_css":{"selectors":["feed","entry",0,"author","name",{"contents":true}],"into":"author_from_xml","error":["CONTENT_MISSING","Could not find author"]}},{"fill":{"with":"/u/%{username_service}","into":"our_author"}},{"assert_compare":{"cmp":"cicmp","a":"author_from_xml","b":"our_author","error":["BAD_USERNAME","Bad post author; wanted '%{our_author}' but got '%{author_from_xml}'"]}},{"selector_css":{"selectors":["feed","entry",0,"title",{"contents":true}],"into":"title","error":["CONTENT_MISSING","Could not find title in XML"]}},{"assert_regex_match":{"pattern":"^.*%{sig_id_medium}.*$","from":"title","error":["TITLE_NOT_FOUND","Missing signature ID (%{sig_id_medium})) in post title '%{title}'"]}},{"selector_css":{"selectors":["feed","entry",0,"content",{"contents":true}],"into":"content_raw","error":["CONTENT_MISSING","Could not find content in XML"]}},{"parse_html":{"from":"content_raw"}},{"selector_css":{"selectors":["code",{"contents":true}],"into":"code_raw","multi":true,"error":["CONTENT_MISSING","Could not find content in XML"]}},{"whitespace_normalize":{"from":"code_raw","into":"code_nw"}},{"replace_all":{"old":" ","new":"\n","from":"code_nw","into":"code"}},{"assert_find_base64":{"needle":"sig","haystack":"code","error":["TEXT_NOT_FOUND","Signature not found in body"]}}]],"rooter":[[{"assert_regex_match":{"pattern":"^https?://[\\w:_\\-\\.]+/_/api/1\\.0/rooter/%{username_service}/.*$","case_insensitive":true,"from":"hint_url"}},{"fetch":{"from":"hint_url","kind":"json"}},{"selector_json":{"selectors":["status","name"],"into":"name"}},{"assert_regex_match":{"pattern":"^ok$","case_insensitive":true,"from":"name"}},{"selector_json":{"selectors":["toot","post"],"into":"post"}},{"assert_regex_match":{"pattern":"^.*%{sig_id_medium}.*$","from":"post"}}]],"twitter":[[{"regex_capture":{"pattern":"^https://(?:twitter|x)\\.com/([^/]+)/status/(\\d+)(.*)$","from":"hint_url","into":["username_from_url","tweet_id","remainder"],"error":["BAD_API_URL","Bad hint from server; URL should start with 'https://twitter.com/%{username_service}/' or 'https://x.com/%{username_service}/'"]}},{"assert_compare":{"cmp":"cicmp","a":"username_from_url","b":"username_service","error":["BAD_API_URL","Bad hint from server; URL should contain username matching %{username_service}; got %{username_from_url}"]}},{"fill":{"with":"https://api.twitter.com/1/statuses/oembed.json?id=%{tweet_id}","into":"our_url"}},{"fetch":{"from":"our_url","kind":"json"}},{"selector_json":{"selectors":["author_url"],"into":"author_url","error":["CONTENT_MISSING","Could not find 'author_url' in json"]}},{"regex_capture":{"pattern":"^https://(?:twitter|x)\\.com/(.+)$","from":"author_url","into":["author_username"],"error":["CONTENT_MISSING","Could not capture username from author_url"]}},{"assert_compare":{"cmp":"cicmp","a":"author_username","b":"username_service","error":["BAD_USERNAME","Bad post authored: wanted '%{username_service}' but got '%{author_username}'"]}},{"selector_json":{"selectors":["html"],"into":"tweet_contents","error":["CONTENT_MISSING","Missing 'tweet_html' in json"]}},{"whitespace_normalize":{"from":"tweet_contents","into":"tweet_contents_nw"}},{"regex_capture":{"pattern":"^.*Verifying myself: I am ([A-Za-z0-9_]+) on (?:(?:https?://)?Keybase\\.io|https://t\\.co/[A-Za-z0-9_-]+)[\\s\\p{Zs}]*\\. (\\S+) */.*$","from":"tweet_contents_nw","into":["username_from_tweet_contents","sig_from_tweet_contents"],"error":["DELETED","Could not find 'Verifying myself: I am %{username_keybase} on Keybase.io. %{sig_id_short}'"]}},{"assert_compare":{"cmp":"cicmp","a":"username_from_tweet_contents","b":"username_keybase","error":["BAD_USERNAME","Wrong username in tweet '%{username_from_tweet_contents}' should be '%{username_keybase}'"]}},{"assert_regex_match":{"pattern":"^%{sig_id_short}$","from":"sig_from_tweet_contents","error":["TEXT_NOT_FOUND","Could not find sig '%{sig_from_tweet_contents}' != '%{sig_id_short}'"]}}]],"generic_web_site":[[{"assert_regex_match":{"pattern":"^%{protocol}://%{hostname}/(?:\\.well-known/keybase\\.txt|keybase\\.txt)$","from":"hint_url","error":["BAD_API_URL","Bad hint from server; didn't recognize API url: \"%{hint_url}\""]}},{"fetch":{"kind":"string","from":"hint_url","into":"blob"}},{"assert_find_base64":{"needle":"sig","haystack":"blob","error":["TEXT_NOT_FOUND","Signature not found in body"]}}]]}}}} +{"kit_version":1,"ctime":1781711007,"tab":{"1":{"pvl_version":1,"revision":1,"services":{"coinbase":[[{"fill":{"with":"x","into":"tmp1"}},{"assert_regex_match":{"pattern":"^y$","from":"tmp1","error":["SERVICE_DEAD","coinbase proofs are no longer supported"]}}]],"dns":[[{"assert_regex_match":{"pattern":"^keybase-site-verification=%{sig_id_medium}$","from":"txt","error":["NOT_FOUND","matching DNS entry not found"]}}]],"facebook":[[{"assert_regex_match":{"pattern":"^[a-zA-Z0-9\\.]+$","from":"username_service","error":["BAD_USERNAME","Invalid characters in username '%{username_service}'"]}},{"regex_capture":{"pattern":"^https://(m|www)\\.facebook\\.com/([^/]*)/posts/([0-9]+)$","from":"hint_url","into":["unused1","username_from_url","post_id"],"error":["BAD_API_URL","Bad hint from server; URL should start with 'https://m.facebook.com/%{username_service}/posts/', got '%{hint_url}'"]}},{"assert_compare":{"cmp":"stripdots-then-cicmp","a":"username_from_url","b":"username_service","error":["BAD_API_URL","Bad hint from server; username in URL should match '%{username_service}', received '%{username_from_url}'"]}},{"fill":{"with":"https://m.facebook.com/%{username_from_url}/posts/%{post_id}","into":"our_url"}},{"fetch":{"kind":"html","from":"our_url"}},{"selector_css":{"selectors":["#m_story_permalink_view",0,"h3",1],"into":"post_text","error":["FAILED_PARSE","Could not find post text in Facebook's response"]}},{"whitespace_normalize":{"from":"post_text","into":"post_text_nw"}},{"regex_capture":{"pattern":"^Verifying myself: I am (\\S+) on Keybase.io. (\\S+)$","from":"post_text_nw","into":["username_from_post","sig_from_post"],"error":["TEXT_NOT_FOUND","Could not find Verifying myself: I am %{username_keybase} on Keybase.io. (%{sig_id_medium})"]}},{"assert_compare":{"cmp":"cicmp","a":"username_from_post","b":"username_keybase","error":["BAD_USERNAME","Wrong keybase username in post '%{username_from_post}' should be '%{username_keybase}'"]}},{"assert_compare":{"cmp":"exact","a":"sig_id_medium","b":"sig_from_post","error":["BAD_SIGNATURE","Could not find sig; '%{sig_from_post}' != '%{sig_id_medium}'"]}},{"selector_css":{"selectors":["#mobile_login_bar",0,"a",0],"attr":"href","into":"join_href","error":["CONTENT_FAILURE","Could not find 'Join' link"]}},{"regex_capture":{"pattern":"^/r\\.php\\?next=https.*facebook.com%2F([^/]*)%2Fposts.*$","from":"join_href","into":["username_from_join_href"],"error":["FAILED_PARSE","Could not interpret 'Join' link"]}},{"assert_compare":{"cmp":"stripdots-then-cicmp","a":"username_from_join_href","b":"username_service","error":["CONTENT_FAILURE","Bad hint from server; username in URL should match '%{username_service}', received '%{username_from_url}'"]}}]],"github":[[{"regex_capture":{"pattern":"^https://gist\\.github(?:usercontent)?\\.com/([^/]*)/.*$","from":"hint_url","into":["username_from_url"],"error":["BAD_API_URL","Bad hint from server; URL should start with either https://gist.github.com OR https://gist.githubusercontent.com"]}},{"assert_compare":{"cmp":"cicmp","a":"username_from_url","b":"username_service","error":["BAD_API_URL","Bad hint from server; URL should contain username matching %{username_service}; got %{username_from_url}"]}},{"fetch":{"kind":"string","from":"hint_url","into":"gist"}},{"assert_find_base64":{"needle":"sig","haystack":"gist"},"error":["TEXT_NOT_FOUND","Signature not found in body"]},{"whitespace_normalize":{"from":"gist","into":"gist_nw"}},{"assert_regex_match":{"pattern":"^((### Verifying myself: I am (https://keybase\\.io)?/%{username_keybase} As part of this verification process, I am signing this object and posting as a gist as github user \\*%{username_service}\\*)|(### Keybase proof I hereby claim: \\* I am %{username_service} on github\\. \\* I am %{username_keybase} \\(https://keybase\\.io/%{username_keybase}\\) on keybase\\.)) .*$","case_insensitive":true,"from":"gist_nw","error":["TEXT_NOT_FOUND","Found sig but gist begins with unexpected content"]}},{"assert_regex_match":{"pattern":"^.*!\\[.*$","negate":true,"case_insensitive":true,"from":"gist_nw","error":["CONTENT_FAILURE","Proof gist must not contain images"]}}]],"hackernews":[[{"regex_capture":{"pattern":"^https://hacker-news\\.firebaseio\\.com/v0/user/([^/]+)/about.json$","from":"hint_url","into":["username_from_url"],"error":["BAD_API_URL","Bad hint from server; URL should match https://hacker-news.firebaseio.com/v0/user/%{username_service}/about.json"]}},{"assert_compare":{"cmp":"cicmp","a":"username_from_url","b":"username_service","error":["BAD_API_URL","Bad hint from server; URL should contain username matching %{username_service}; got %{username_from_url}"]}},{"fetch":{"kind":"string","from":"hint_url","into":"profile"}},{"assert_regex_match":{"pattern":"^.*%{sig_id_medium}.*$","from":"profile","error":["TEXT_NOT_FOUND","Posted text does not include signature '%{sig_id_medium}'"]}}]],"reddit":[[{"regex_capture":{"pattern":"^https://www\\.reddit\\.com/r/([^/]+)/(.*)/(\\.json)$","from":"hint_url","into":["subreddit_from_url","path_remainder","json_trailer"],"error":["BAD_API_URL","URL should start with 'https://www.reddit.com/r/keybaseproofs'"]}},{"assert_regex_match":{"pattern":"^keybaseproofs$","case_insensitive":true,"from":"subreddit_from_url","error":["BAD_API_URL","URL contained wrong subreddit '%{subreddit_from_url}' != 'keybaseproofs'"]}},{"fill":{"with":"https://old.reddit.com/r/%{subreddit_from_url}/%{path_remainder}/","into":"our_url"}},{"fetch":{"from":"our_url","kind":"html"}},{"selector_css":{"selectors":["div.thing.link.self"],"attr":"data-fullname","into":"post_id","error":["FAILED_PARSE","Could not find entry element"]}},{"assert_regex_match":{"pattern":"^t3_.+$","from":"post_id","error":["CONTENT_FAILURE","Wanted an entry ID with 't3' prefix but got '%{post_id}'"]}},{"selector_css":{"selectors":["div.thing.link.self"],"attr":"data-subreddit","into":"post_subreddit","error":["FAILED_PARSE","Could not find entry element"]}},{"assert_regex_match":{"pattern":"^keybaseproofs$","case_insensitive":true,"from":"post_subreddit","error":["CONTENT_FAILURE","Wrong subreddit %{post_subreddit}"]}},{"selector_css":{"selectors":["div.thing.link.self"],"attr":"data-author","into":"post_author","error":["FAILED_PARSE","Could not find entry element"]}},{"assert_compare":{"cmp":"cicmp","a":"post_author","b":"username_service","error":["BAD_USERNAME","Bad post author; wanted '%{username_service}' but got '%{post_author}'"]}},{"selector_css":{"selectors":["html","head","title",{"contents":true}],"into":"title","error":["TITLE_NOT_FOUND","Could not find title in HTML"]}},{"assert_regex_match":{"pattern":"^.*%{sig_id_medium}.*$","multiline":true,"from":"title","error":["TITLE_NOT_FOUND","Missing signature ID (%{sig_id_medium}) in post title '%{title}'"]}},{"selector_css":{"selectors":["div.thing.link.self","div.usertext-body"],"into":"post_body","error":["CONTENT_MISSING","Could not find entry element"]}},{"assert_find_base64":{"needle":"sig","haystack":"post_body","error":["TEXT_NOT_FOUND","Signature not found in body"]}}]],"rooter":[[{"assert_regex_match":{"pattern":"^https?://[\\w:_\\-\\.]+/_/api/1\\.0/rooter/%{username_service}/.*$","case_insensitive":true,"from":"hint_url"}},{"fetch":{"from":"hint_url","kind":"json"}},{"selector_json":{"selectors":["status","name"],"into":"name"}},{"assert_regex_match":{"pattern":"^ok$","case_insensitive":true,"from":"name"}},{"selector_json":{"selectors":["toot","post"],"into":"post"}},{"assert_regex_match":{"pattern":"^.*%{sig_id_medium}.*$","from":"post"}}]],"twitter":[[{"regex_capture":{"pattern":"^https://(?:twitter|x)\\.com/([^/]+)/status/(\\d+)(.*)$","from":"hint_url","into":["username_from_url","tweet_id","remainder"],"error":["BAD_API_URL","Bad hint from server; URL should start with 'https://twitter.com/%{username_service}/' or 'https://x.com/%{username_service}/'"]}},{"assert_compare":{"cmp":"cicmp","a":"username_from_url","b":"username_service","error":["BAD_API_URL","Bad hint from server; URL should contain username matching %{username_service}; got %{username_from_url}"]}},{"fill":{"with":"https://api.twitter.com/1/statuses/oembed.json?id=%{tweet_id}","into":"our_url"}},{"fetch":{"from":"our_url","kind":"json"}},{"selector_json":{"selectors":["author_url"],"into":"author_url","error":["CONTENT_MISSING","Could not find 'author_url' in json"]}},{"regex_capture":{"pattern":"^https://(?:twitter|x)\\.com/(.+)$","from":"author_url","into":["author_username"],"error":["CONTENT_MISSING","Could not capture username from author_url"]}},{"assert_compare":{"cmp":"cicmp","a":"author_username","b":"username_service","error":["BAD_USERNAME","Bad post authored: wanted '%{username_service}' but got '%{author_username}'"]}},{"selector_json":{"selectors":["html"],"into":"tweet_contents","error":["CONTENT_MISSING","Missing 'tweet_html' in json"]}},{"whitespace_normalize":{"from":"tweet_contents","into":"tweet_contents_nw"}},{"regex_capture":{"pattern":"^.*Verifying myself: I am ([A-Za-z0-9_]+) on (?:(?:https?://)?Keybase\\.io|https://t\\.co/[A-Za-z0-9_-]+)[\\s\\p{Zs}]*\\. (\\S+) */.*$","from":"tweet_contents_nw","into":["username_from_tweet_contents","sig_from_tweet_contents"],"error":["DELETED","Could not find 'Verifying myself: I am %{username_keybase} on Keybase.io. %{sig_id_short}'"]}},{"assert_compare":{"cmp":"cicmp","a":"username_from_tweet_contents","b":"username_keybase","error":["BAD_USERNAME","Wrong username in tweet '%{username_from_tweet_contents}' should be '%{username_keybase}'"]}},{"assert_regex_match":{"pattern":"^%{sig_id_short}$","from":"sig_from_tweet_contents","error":["TEXT_NOT_FOUND","Could not find sig '%{sig_from_tweet_contents}' != '%{sig_id_short}'"]}}]],"generic_web_site":[[{"assert_regex_match":{"pattern":"^%{protocol}://%{hostname}/(?:\\.well-known/keybase\\.txt|keybase\\.txt)$","from":"hint_url","error":["BAD_API_URL","Bad hint from server; didn't recognize API url: \"%{hint_url}\""]}},{"fetch":{"kind":"string","from":"hint_url","into":"blob"}},{"assert_find_base64":{"needle":"sig","haystack":"blob","error":["TEXT_NOT_FOUND","Signature not found in body"]}}]]}}}} diff --git a/pvl-tools/tab/1.cson b/pvl-tools/tab/1.cson index 23a531423d82..9a4a1814a777 100644 --- a/pvl-tools/tab/1.cson +++ b/pvl-tools/tab/1.cson @@ -179,99 +179,76 @@ services: , from: "hint_url" , into: ["subreddit_from_url", "path_remainder", "json_trailer"] , error: ["BAD_API_URL", "URL should start with 'https://www.reddit.com/r/keybaseproofs'"] } }, - # this assertion is just here to test the pvl updater - { assert_regex_match: { - , pattern: "^.*reddit.*$" - , from: "hint_url" - , error: ["BAD_API_URL", "URL should contain 'reddit'"] } }, { assert_regex_match: { , pattern: "^keybaseproofs$" , case_insensitive: true , from: "subreddit_from_url" - , error: ["BAD_API_URL", "URL contained wrong subreddit '%{subreddit_from_url}' !+ 'keybaseproofs'"] } }, + , error: ["BAD_API_URL", "URL contained wrong subreddit '%{subreddit_from_url}' != 'keybaseproofs'"] } }, # Create the old.reddit.com url to fetch from { fill: { - , with: "https://old.reddit.com/r/%{subreddit_from_url}/%{path_remainder}/.rss" + , with: "https://old.reddit.com/r/%{subreddit_from_url}/%{path_remainder}/" , into: "our_url" } }, { fetch: { , from: "our_url" , kind: "html" } }, - # Index to the first entry: reddit's .rss for a comments page returns the - # submission as entry 0 followed by comment entries. The t3_ check below - # confirms entry 0 is the post (comments are t1_), so we fail closed if the - # ordering ever changes. + # Select the submission itself: on an old.reddit.com comments page the post + # is the single `link self` thing, while comments are separate `comment` + # things. We read its fullname and the t3_ check below confirms it really is + # a submission (t3_) and not a comment (t1_), so we fail closed if the markup + # ever changes. { selector_css: { - , selectors: ["feed", "entry", 0, "id", { contents: true }] - , into: "entry_id" - , error: ["CONTENT_MISSING", "Could not find entry ID"] } }, + , selectors: ["div.thing.link.self"] + , attr: "data-fullname" + , into: "post_id" + , error: ["FAILED_PARSE", "Could not find entry element"] } }, { assert_regex_match: { , pattern: "^t3_.+$" - , from: "entry_id" - , error: ["CONTENT_FAILURE", "Wanted an entry ID with 't3' prefix but got '%{entry_id}'"] } }, + , from: "post_id" + , error: ["CONTENT_FAILURE", "Wanted an entry ID with 't3' prefix but got '%{post_id}'"] } }, # check the subreddit { selector_css: { - , selectors: ["feed", "entry", 0, "category"] - , attr: "label" - , into: "subreddit_from_xml" - , error: ["CONTENT_MISSING", "Could not find category (subreddit)"] } }, + , selectors: ["div.thing.link.self"] + , attr: "data-subreddit" + , into: "post_subreddit" + , error: ["FAILED_PARSE", "Could not find entry element"] } }, { assert_regex_match: { - , pattern: "^r/keybaseproofs$" + , pattern: "^keybaseproofs$" , case_insensitive: true - , from: "subreddit_from_xml" - , error: ["CONTENT_FAILURE", "Wrong subreddit %{subreddit_from_xml}"] } }, + , from: "post_subreddit" + , error: ["CONTENT_FAILURE", "Wrong subreddit %{post_subreddit}"] } }, # check the author { selector_css: { - , selectors: ["feed", "entry", 0, "author", "name", { contents: true }] - , into: "author_from_xml" - , error: ["CONTENT_MISSING", "Could not find author"] } }, - { fill: { - , with: "/u/%{username_service}" - , into: "our_author" } }, + , selectors: ["div.thing.link.self"] + , attr: "data-author" + , into: "post_author" + , error: ["FAILED_PARSE", "Could not find entry element"] } }, { assert_compare: { , cmp: "cicmp" - , a: "author_from_xml" - , b: "our_author" - , error: ["BAD_USERNAME", "Bad post author; wanted '%{our_author}' but got '%{author_from_xml}'"] } }, + , a: "post_author" + , b: "username_service" + , error: ["BAD_USERNAME", "Bad post author; wanted '%{username_service}' but got '%{post_author}'"] } }, # check the title { selector_css: { - , selectors: ["feed", "entry", 0, "title", { contents: true }] + , selectors: ["html", "head", "title", { contents: true }] , into: "title" - , error: ["CONTENT_MISSING", "Could not find title in XML"] } }, + , error: ["TITLE_NOT_FOUND", "Could not find title in HTML"] } }, { assert_regex_match: { , pattern: "^.*%{sig_id_medium}.*$" + , multiline: true , from: "title" - , error: ["TITLE_NOT_FOUND", "Missing signature ID (%{sig_id_medium})) in post title '%{title}'"] } }, - # check the content - { selector_css: { - , selectors: ["feed", "entry", 0, "content", { contents: true }] - , into: "content_raw" - , error: ["CONTENT_MISSING", "Could not find content in XML"] } }, - { parse_html: { - , from: "content_raw" } }, - # Proof signatures are always in blocks. There may be more than one - # code block in the post, though (see max's proof), extract and concatenate - # them all into one register. + , error: ["TITLE_NOT_FOUND", "Missing signature ID (%{sig_id_medium}) in post title '%{title}'"] } }, + # Grab the rendered post body. The signature lives in /
 blocks
+    # whose newlines are preserved in the HTML, so pulling the whole usertext-body
+    # text into one register is enough to recover a multi-line PGP signature
+    # (see max's proof) as well as single-line NaCl ones.
     { selector_css: {
-      , selectors: ["code", { contents: true }]
-      , into: "code_raw"
-      , multi: true
-      , error: ["CONTENT_MISSING", "Could not find content in XML"] } },
-    # The content in .rss endpoint is stripped out of newlines. We are going to
-    # normalize whitespace and replace all spaces with newlines to recover the
-    # matching multiline PGP signature so it matches the `sig` register. This has
-    # no effect on NaCl signatures.
-    { whitespace_normalize: {
-      , from: "code_raw"
-      , into: "code_nw" } },
-    { replace_all: {
-      , old: " "
-      , new: "\n"
-      , from: "code_nw"
-      , into: "code" } },
-    # Finally, find the signature in the normalized text.
+      , selectors: ["div.thing.link.self", "div.usertext-body"]
+      , into: "post_body"
+      , error: ["CONTENT_MISSING", "Could not find entry element"] } },
+    # Finally, find the signature in the post body.
     { assert_find_base64: {
       , needle: "sig"
-      , haystack: "code"
+      , haystack: "post_body"
       , error: ["TEXT_NOT_FOUND", "Signature not found in body"] } },
   ]]
   rooter: [[