Salta al contenuto

Piero Bosio Social Web Site Personale Logo Fediverso

Social Forum federato con il resto del mondo. Non contano le istanze, contano le persone
  • 1 Votazioni
    1 Post
    0 Visualizzazioni
    blog@shkspr.mobi
    A reasonably practical guide to validating RFC 9421 HTTP Signatures for ActivityPub in PHP https://shkspr.mobi/blog/2026/09/a-reasonably-practical-guide-to-validating-rfc-9421-http-signatures-for-activitypub-in-php/ If you're reading this, you've probably been hitting your head against a brick wall trying to parse and decipher the new HTTP Signatures sent by Mastodon and other Fediverse servers.This is a basic and somewhat incomplete guide to accepting these signatures. I'm sure there are various gotchas, but it works with the signatures I've seen in the wild.Shut Up And Show Me The Code!OK, wow, no need to be a dick about it! Here's how I validated a real signature that my server received. PHP$verified = openssl_verify( data: '"@method": POST"@target-uri": https://example.viii.fi/inbox"content-digest": sha-256=:tFdB/ENGczHMlZMDb66pXoUi2d0OqH2iBHdnN/WV1mc=: "@signature-params": ("@method" "@target-uri" "content-digest");created=1787780262;keyid="https://mastodon.social/users/Edent#main-key"', signature: base64_decode( "sIfmNsM/Q8iG6AJlne1IkZVjQSVFDEYIPsnoSOXQY+W3Eb4+SOn9o4J5SQmFOP+Jecjf3ioFwUdsrFjAGkUUOHPvSbNWkGKtNuGm+C6r3aI3JBCFGPqX3ITgZYV76CF7JJJ5hPGaG8YH/XdmxVIeFfD3M39FQCncMyyq7xJJvwKKP1mzS5s1vNQie8hbQ9owRjtqvoWcmM9GEYCUHNcMPLjZc+CBrj8sfBbNTYgIFI4UtirOaRJvYymxXjmXuzeVYxQujMjAjgobxQ8QFv0zlYsHk+gS5EYyafpJG9zmfCFSoF9+ZwqKNADmuADbISD9LZIH/bmkPoNXhxaeFPqYog==" ), public_key: "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsYMEs4waqk/6gaS+xn1T\nYygElTtNIFNkBcEdEBMaeoGVhyZiVKtSjJCS4z+X+394PKvcfSTcFILIt2GI2jOB\nHD0M2fFgxc8mmdSdCQkgEh9jF3bFI3kopDvzYf726iioYKlHXKpfPKvFt7EJgKH7\naCtS25NQkek3YUd6y3VBcT3R6Xhze9P3QNoZMIsFXklgXDKj+EllfbUqLf1vxt3s\nmD9ETxy2bJi9FheE0uY2WhARn49XAvwczM5Wzt+zqxVEtgpi5v2+ZZAVKhDnJkiC\nCCuI6hrSnKNIx/5mSlX0a0S5h5d03djrCkYsqmwelu01rhOXP2grsz4BXp0y2wrO\n3QIDAQAB\n-----END PUBLIC KEY-----\n", algorithm: "sha256" ); echo $verified; Copy and paste that into PHP and you should see that $verified is true.NOW EXPLAIN THE CODESay please.PLEASE!!!Along with the message sent to your server, you will have received HTTP headers like this: content-digest: sha-256=:tFdB/ENGczHMlZMDb66pXoUi2d0OqH2iBHdnN/WV1mc=: signature: sig1=:sIfmNsM/Q8iG6AJlne1IkZVjQSVFDEYIPsnoSOXQY+W3Eb4+SOn9o4J5SQmFOP+Jecjf3ioFwUdsrFjAGkUUOHPvSbNWkGKtNuGm+C6r3aI3JBCFGPqX3ITgZYV76CF7JJJ5hPGaG8YH/XdmxVIeFfD3M39FQCncMyyq7xJJvwKKP1mzS5s1vNQie8hbQ9owRjtqvoWcmM9GEYCUHNcMPLjZc+CBrj8sfBbNTYgIFI4UtirOaRJvYymxXjmXuzeVYxQujMjAjgobxQ8QFv0zlYsHk+gS5EYyafpJG9zmfCFSoF9+ZwqKNADmuADbISD9LZIH/bmkPoNXhxaeFPqYog==: signature-input: sig1=("@method" "@target-uri" "content-digest");created=1787780262;keyid="https://mastodon.social/users/Edent#main-key" The signature-input tells you how to construct a "Signature Base". You have to build a text string which places the various components in the order specified and separated with a newline: "@method": POST "@target-uri": https://example.viii.fi/inbox "content-digest": sha-256=:tFdB/ENGczHMlZMDb66pXoUi2d0OqH2iBHdnN/WV1mc=: "@signature-params": ("@method" "@target-uri" "content-digest");created=1787780262;keyid="https://mastodon.social/users/Edent#main-key" Where @method is the HTTP method used to send data to your server (usually GET or POST), and @target-uri is the URl the message was sent to (usually your inbox).The publicKey is slightly trickier. As you can see, the signature-input ends with keyid="https://mastodon.social/users/Edent#main-keyIf you make a signed request to that URl, you'll get back an ActivityPub Actor document. It will look something like this: JSON{ "@context": [ "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", ], "id": "https://mastodon.social/users/Edent", "webfinger": "Edent@mastodon.social", "type": "Person", "name": "Terence Eden", "publicKey": { "id": "https://mastodon.social/users/Edent#main-key", "owner": "https://mastodon.social/users/Edent", "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsYMEs4waqk/6gaS+xn1T\nYygElTtNIFNkBcEdEBMaeoGVhyZiVKtSjJCS4z+X+394PKvcfSTcFILIt2GI2jOB\nHD0M2fFgxc8mmdSdCQkgEh9jF3bFI3kopDvzYf726iioYKlHXKpfPKvFt7EJgKH7\naCtS25NQkek3YUd6y3VBcT3R6Xhze9P3QNoZMIsFXklgXDKj+EllfbUqLf1vxt3s\nmD9ETxy2bJi9FheE0uY2WhARn49XAvwczM5Wzt+zqxVEtgpi5v2+ZZAVKhDnJkiC\nCCuI6hrSnKNIx/5mSlX0a0S5h5d03djrCkYsqmwelu01rhOXP2grsz4BXp0y2wrO\n3QIDAQAB\n-----END PUBLIC KEY-----\n" }, The publicKeyPem is the string you need. There's no need to convert the \n to literal newlines.Is that it?Not quite! All we've done so far is verify the headers. It is possible that these are genuine headers but attached to a fraudulent body.This takes us back to the header "content-digest": sha-256=:tFdB/ENGczHMlZMDb66pXoUi2d0OqH2iBHdnN/WV1mc=:That says that the body of the message sent has a Base64 encoded SHA256 hash of tFdB/ENGczHMlZMDb66pXoUi2d0OqH2iBHdnN/WV1mc=.To calculate your own content digest in PHP: PHP$digestCalculated = base64_encode( hash( algo: "sha256", data: $body, binary: true ) ); Does your digest match the one sent along with the headers? If not, something dodgy is going on.Putting it all togetherThe steps are:Get the headers.Get the body.From the headers' content-digest extract the algorithm and hash.Using the body, calculate your own hash using the algorithm from content-digest.Does your hash match the sent hash? If not, stop. If so, proceed.From the headers' signature extract the base64 encoded signature.From the headers' signature-input extract the signature-input string.From the signature-input string extract the order of the Signature Base.Construct the Signature Base.From the signature-input string extract the keyid.Get the Public Key from the keyid.Use openssl_verify() to verify the Signature Base and the base64 decoded signature, against the Public Key using SHA256.Note, Mastodon only uses SHA256. I think it should explicitly say which algorithm it is using and have raised the issue.In Code FormThis is how you do it in PHP. Please read this carefully as there are some hard-coded assumptions. PHP<?php// Validate the Digest.// It is the hash of the raw input string, in binary, encoded as base64.// The format is content-digest => <algorithm>=:<base64 encoded hash>:$digestString = $headers["content-digest"]; // The Base64 encoding may have multiple `=` at the end. So split this at the first `=`.$digestData = explode( separator: "=", string: $digestString, limit: 2 ); // Hashes are in lowercase, but have a `-` in their name.// This is not what hash_algos() expects.$digestAlgorithm = str_replace( search: "-", replace: "", subject: $digestData[0] ); // The hash is surrounded by `:` characters.$digestHash = str_replace( search: ":", replace: "", subject: $digestData[1] ); // Check if the hash algorithm is one known about to PHP.// If not, reject and record an error.if ( !in_array( needle:$digestAlgorithm, haystack: hash_algos() ) ) { return false; } // Manually calculate the digest based on the data sent.$digestCalculated = base64_encode( hash( algo: $digestAlgorithm, data: $input, binary: true ) ); // Does our calculation match what was sent?if ( !( $digestCalculated == $digestHash ) ) { return false; } // The signature format is signature => <signature name>=:<base64 encoded hash>:$signatureString = $headers["signature"]; // The Base64 encoding may have multiple `=` at the end. So split this at the first `=`.$signatureData = explode( separator: "=", string: $signatureString, limit: 2 ); $signatureName = $signatureData[0]; // The signature is surrounded by `:` characters.$signatureB64 = str_replace( search: ":", replace: "", subject: $signatureData[1] ); // The signature-input format is complicated!$signatureInputString = $headers["signature-input"]; // Get the parameters. Assume there is only one signature.$signatureParamsString = explode( separator: "=", string: $signatureInputString, limit: 2 )[1]; // Get the different elements of the signature.$signatureInputData = explode( separator: ";", string: $signatureInputString ); // Construct the data.$signatureInput = []; foreach( $signatureInputData as $signatureInputParts ) { $partsData = explode( separator: "=", string: $signatureInputParts ); // Strip quotes from keyid and parentheses from sig1. if ( "keyid" == $partsData[0] ) { $partsData[1] = str_replace( search: "\"", replace: "", subject: $partsData[1] ); } if ( $signatureName == $partsData[0] ) { $partsData[1] = str_replace( search: ["(", ")"], replace: "", subject: $partsData[1] ); } $signatureInput[ $partsData[0] ] = $partsData[1] ; } $signatureStructure = $signatureInput[$signatureName]; $signatureKeyID = $signatureInput["keyid"]; // Remove quotes.$signatureStructure = str_replace( search: "\"", replace: "", subject: $signatureStructure ); $signatureStructureData = explode( separator: " ", string: $signatureStructure ); // https://www.rfc-editor.org/info/rfc9421/#section-2.5$signatureBase = ""; foreach ( $signatureStructureData as $signatureStructureParts ) { if ( "@method" == $signatureStructureParts ) { // https://www.rfc-editor.org/info/rfc9421/#name-method $signatureBase .= "\"@method\": " . strtolower( $_SERVER["REQUEST_METHOD"] . "\n" ); } if ( "@target-uri" == $signatureStructureParts ) { // https://www.rfc-editor.org/info/rfc9421/#section-2.2.2 // Change the domain name to your own. $signatureBase .= "\"@target-uri\": https://EXAMPLE.COM" . $_SERVER["REQUEST_URI"] . "\n"; } if ( "content-digest" == $signatureStructureParts ) { $signatureBase .= "\"content-digest\": $digestString\n"; } } // https://victoronsoftware.com/posts/http-message-signatures/#how-the-signature-is-created$signatureBase .= "\"@signature-params\": $signatureParamsString"; // Get the signing user's public key.// This is usually in the form `https://example.com/user/username#main-key`// This is to differentiate if the user has multiple keys.// This may need to be a signed request. You will need to write your own getDataFromURl() function to get the sending user's key.$userData = getDataFromURl( $signatureKeyID ); $publicKey = $userData["publicKey"]["publicKeyPem"]; // Verify the request$verified = openssl_verify( data: $signatureBase, signature: base64_decode( $signatureB64 ), public_key: $publicKey, algorithm: $digestAlgorithm ); // Convert the result to boolean.if ( $verified === 1 ) { $verified = true; } elseif ( $verified === 0 ) { $verified = false; } else { $verified = null; } return $verified; Further ReadingRFC 9421 HTTP Message SignaturesUnderstanding HTTP message signatures: A developer's guideSign and verify HTTP messages (RFC 9421)Verification of HTTP Message SignaturesHTTP-Message-Signer in PHPThanks to NLnetThis blog post was funded in part by the work I'm doing for my NLnet NGI0 grant. Thanks! #ActivityBot #ActivityPub #mastodon #php #webdev
  • 0 Votazioni
    4 Post
    0 Visualizzazioni
    michael@mstdn.thms.uk
    @Crell yeah, I kinda wish mastodon would generate a notification along the lines of ‘garfieldtech.com/.../.. has claimed you authored this post. do you want to add it to your verified sources?’ or something like that.but I'm sure that would get abused too …
  • 1 Votazioni
    1 Post
    12 Visualizzazioni
    len@toot.si
    I wonder whether there are any opportunities (for me) to work with #activitypub and #fediverse. My background is in #PHP (decoupled #Drupal), but I am keen to diversify into other languages. I also have solid experience with #REST, #bash and CLI tools.#Fedihire
  • Functional programming isn't just for Haskell developers.

    Mondo php
    1
    0 Votazioni
    1 Post
    0 Visualizzazioni
    crell@phpc.social
    Functional programming isn't just for Haskell developers. It's for #PHP developers, too. "Thinking Functionally in PHP" is available from LeanPub.https://leanpub.com/thinking-functionally-in-php
  • Is there anything decent for #Prometheus and #PHP monitoring?

    Mondo prometheus php nginx
    7
    0 Votazioni
    7 Post
    0 Visualizzazioni
    falkheiland@fosstodon.org
    @pertho are there new metrics that were added to PHP in the last 4 years that you need to scrape? Otherwise you can probably just take an "old" exporter. For logs you would take something other then Prometheus like elk stack, wazuh etc
  • So have put enough effort into this for now.

    Mondo php
    1
    1
    0 Votazioni
    1 Post
    0 Visualizzazioni
    dantleech@fosstodon.org
    So have put enough effort into this for now. #php package to visualize/survey installed composer dependencies:https://github.com/dantleech/depgraph
  • 0 Votazioni
    1 Post
    0 Visualizzazioni
    grmpyprogrammer@yac.grumpy-learning.com
    Well, I just got let go from my job after 2.5years so it’s time to find the next thing.I have almost 30 years experience with #PHP and for many years was one of the community’s loudest voices about testing and code quality.I like helping teams turn business requirements into working software and refactor applications and modernize them to conform to patterns and practices that lead to better outcomes.I am looking for 100% remote roles where I can use my PHP (with a bit of Python ) skills to help your project get better.I am in the North American Eastern time zone and have dealt with jobs where folks were both in EU time zones and North American Pacific time zone.I am eligible to work in Canada and I also have a consulting business so non-Canadian companies could use me.#getfedihired #fedihire
  • Happy 31st Birthday to the #PHP programming language.

    Mondo php
    1
    0 Votazioni
    1 Post
    0 Visualizzazioni
    shawnhooper@fosstodon.org
    Happy 31st Birthday to the #PHP programming language. https://en.wikipedia.org/wiki/PHP
  • About to start a solo web dev project.

    Mondo webdev kanban php javascript
    6
    0 Votazioni
    6 Post
    0 Visualizzazioni
    aamfp@fosstodon.org
    @sephsterPen and paper? 😇https://marcoxbresciani.codeberg.page/kaizen/kanban-journal.html
  • Ci sono programmatori PHP che usavano Visual Studio Code?

    Mondo php microsoft opensource
    4
    0 Votazioni
    4 Post
    8 Visualizzazioni
    aamfp@fosstodon.org
    @bozOddio NetBeans è ancora vivo?!
  • 0 Votazioni
    22 Post
    0 Visualizzazioni
    gina@fosstodon.org
    @pndc Have you considered the Belastingdienst or UWV/SVB? I know that they are dealing with a lot of legacy stuff.