cosign icon indicating copy to clipboard operation
cosign copied to clipboard

parse the rekor bundle correctly

Open viveksahu26 opened this issue 1 year ago • 6 comments

Summary

This PR fixes the bugs while attaching the rekor-bundle into an image. Closes #3458

Release Note

Bug fixes and fixes of previous known issues

Documentation

  • Use OpenSSL tool to generate key pair.
  • private key: openssl genrsa -out privkey.pem 4096
  • public key: openssl rsa -in privkey.pem -outform PEM -pubout -out pubkey.pem
  • IMAGE_DIGEST=ghcr.io/viveksahu26/hi-cosign:main
  • Generate Payload of an Image
    • cosign generate $IMAGE_DIGEST > payload.json
  • Now sign the payload, using private key:
    • openssl dgst -sha256 -sign privkey.pem -out payload.json.sig payload.json
  • Convert the signature into base64
    • cat payload.json.sig | base64 > payload.json.base64.sig
  • Let's check the manifest of image:
    • NEW_IMAGE=$(cosign triangulate ${IMAGE_DIGEST})
    • crane manifest ${NEW_IMAGE}
  • Now, let's upload payload, signature, and public key to rekor instance.
    • rekor-cli upload --artifact payload.json --signature payload.json.sig --public-key pubkey.pem --pki-format x509
    • You will get o/p as logIndex or uuid of the record.
  • Finally, rekor-bundle is added. Now check the rekor-bundle:
    • check whether record is uploaded or not: rekor-cli get --log-index 60606559
  • Now, let's attach this rekor-bundle to an Image, for that first you need to create a rekor-bundle by yourself. - curl https://rekor.sigstore.dev/api/v1/log/entries\?logIndex\=60606559 | jq
    • Now construct the rekor-bundle:
      at rekor_bundle.json | jq
      
      "SignedEntryTimestamp": "MEYCIQDCBEsMQKGMopTKw9/NNnxUNqEPcmJotc7VuRlkcSaS2gIhAIoHOgkFXIOy2rI843w79yLVYc6/M/QMUApLvbFcF7Qj",
      "Payload": {
      "body": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoicmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI0ODEwZmQ5YjEzODdlNDZhZWMyM2NmNTYwNjU3OTcxOTllZmU3NzM3YmQ0NmNiMTViYzI0Y2VkMjY5MGNhYzUyIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6IkdPOUlIcCsyemkvNUI4SEZWWkVYQStIN0JIVGxBZm1OQ2FMbGNxeFhLaENMQmdmazZjNFo4UkFGUnFodGVOWGMxNzlIaXkweSsvbjR5d0tyc2Q4UFh2Y3VObmlLM2tneXhXYzJHNWowTjJBRE85QStZY2U2NkUrcTN4MkQyU0FveWVMcnpkby9Qa2lmVDR1dnVZK0o1NVlMUW9XRG5nMExid1NVVGVGOGpHRXdMN3pKUTRzZzhOSnhvMFMyVStWdlZLSVNoRzFFQWlBOUMxZ2M1NWhHT29yTmFhZjhEL3JTM0V5THFIS3EzL1N1REhseHAzUVk2clU4QlZJTlVWZ2lUbWJ6WHY2eW83NnJVMmsyampwOWFaWlRiK1F2UWdWbGV6WGwrVld5U0thb0pxQ3RRNy9mMk5FdFplZ1VUSnJRQTdnMERrbXoyMUFDMzk4YlZpMHRmUDF2aXZEelh4TkMxNXBtL2tYdWk2ejcxOUtHazhxN3NRMDVyY3ZaZm9pMERmRmNZbDB0aFA4K1dsRU5kdU5zRnJtK1ZQditaYjBCbnFHM0twTkZ0Ymdtc092WXFvNHAyZjFlb1IwTFNEMlp3cEFRNERUd3dRRGh5UEk3MzdWT2ZJc1hObHg2T0srVml5Vmg4cGhRZHJrK2doRXJJVEM0WFllYVlRaUNvWTgySWNDcEh3Mmw3QS9SeDdiNXdVMDhnVkg2S1JjWmJFNm9mVStmSFFTcTQ4OHhvNnQrUHdLWHVDN1RtUFhJLzB4Uys4aDJ4bXRqOCtXOHRRQzFZRHcwU3JGZzIyelBFMkRjR3Z5KytIQXNpazd6VmEzNlovN2VzcndSOWpxdE8vd1M3UnAwQVMvenNjZVdiWTFoVEhVcXNsbE9FYU9DYThoN2NnUCtQTWdSV0dZPSIsImZvcm1hdCI6Ing1MDkiLCJwdWJsaWNLZXkiOnsiY29udGVudCI6IkxTMHRMUzFDUlVkSlRpQlFWVUpNU1VNZ1MwVlpMUzB0TFMwS1RVbEpRMGxxUVU1Q1oydHhhR3RwUnpsM01FSkJVVVZHUVVGUFEwRm5PRUZOU1VsRFEyZExRMEZuUlVGdFRsVk9SRWxVZHpKcVUzWmpZbEVyYjBwMU5ncGlaMWN2Tm5ZNFdHTk9WRmRoTHpKdWRUbFVVbVJTYldONmJtVnZWMVE1VGtKclZWcEZPV2hZYVU1U0t5OUNOVTVyTlZwa2RtSjJWVEUwYmtFNVJqSmtDalZJVURKS1UxUkNTVVZ5Y0hSblNVVnpTMHRaYkhKblJrRjVUSEYxWWtGbllYSXJZWEJLY0U5QmFHOXZXa1pIV1ZOTlRDOXZVWGxXTVdoUlYwdEdiRkFLTUd4aldFazNPRFJNU0dwemNubE9iM2xCUmxoR1JuazBhR1pCUXpaVE4zTTVVRTFQUjFWU1RVNXdibU5YYkZseFpqaFFUMVJCUlZSaVRETjRVa0lyUkFwU1QxQk1Za3ROVjNoNVVIbFRTR0ZuVW5JMlJHUktOVUZyYW5oMVUwWldUelFyZG1Kb2ExaFlURTUxTkZKcWFFVlVibE5LVDNJMVYzWjZiREF5T0ZjekNqVk1haTkyV0ZWNFoyMUNjV1F2Y0dKS01EQklNVko0Ym14V0wwOHlOV3B2Tm1aNVRXUmlPVzFxTjNKUlpHUnVWMUpqVVhGU0szQTJjVWR2Ukd4NU5UY0tSRFIzVm1SaWJGVk9WbWRyTUd4T1NrOWplbkl2WTB3dmNHSllTa28yYW5kMEt6UkNhbGxYVjA1VmVIUTNNM2hDTkhSTE4yZE9aVWN3ZUZReWNGSklNQW81U2pGWVZrWTViblJtUjNWcVJuTm1VeXRVTUhCemFYbHpNRFJ2V2twWE0xRlBUbE15UkRFek9IWTFRVWhaVDJsVGJXMUNSakJtUVdOUE5qUm9iMDA1Q2pJNVJFdE9URTlEUldsbWIydFBkMjlyVkROQmVrMHZaREl4WlZBd2MxbHZOV05ZT1hKR1NGSmxhVGdyZEVGWWF6aGpVMnBZWjI5eE1rRXdabEZNVFdJS2RFWjZWVXA1YkU5elltSjJTV3N6VUV0S2VXeFpSMDVDZG5rek56SkdkRlV3ZWtaTVVWQlRiMW93TWxaSFVTODBWbXR0YVZScU5GcFRkakZaYlV4UVNRcFhiVE01ZUVkd2FEaFpVbVpGUVhKR1JWRlNOeTlLV2tad2JVc3pPV1ZwTkZsWmVsRndjRXhqVW1sRVdHUjBia0pEV1VSTE0ydHVSekZzWnpKWVIyRnpDazF0V0VsdFFYQlFTMWcwTDA5a1RVdzVaVXMwY0dZd1EwRjNSVUZCVVQwOUNpMHRMUzB0UlU1RUlGQlZRa3hKUXlCTFJWa3RMUzB0TFFvPSJ9fX19",
      "integratedTime": "1704106175",
      "logIndex": "60606559",
      "logID": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d"
      }
      
      
      

NOTE: currently there is no such commands which automates the process of constructing a rekor bundle for us. Like cosign generate command create the payload on providing the image. - Now upload this rekor-bundle, signature and payload to an Image. - cosign attach signature --payload payload.json --signature payload.json.base64.sig --rekor-response rekor_bundle.json $IMAGE_DIGEST

  • Check the manifest of image, whether the rekor-bundle is added to the image or not.
    • NEW_IMAGE=$(cosign triangulate ${IMAGE_DIGEST})
    • crane manifest ${NEW_IMAGE}

viveksahu26 avatar Jan 02 '24 07:01 viveksahu26

Codecov Report

Attention: 5 lines in your changes are missing coverage. Please review.

Comparison is base (628df78) 40.10% compared to head (7b91e59) 40.06%. Report is 7 commits behind head on main.

Files Patch % Lines
cmd/cosign/cli/options/attach.go 0.00% 4 Missing :warning:
cmd/cosign/cli/attach.go 0.00% 1 Missing :warning:
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3461      +/-   ##
==========================================
- Coverage   40.10%   40.06%   -0.04%     
==========================================
  Files         155      155              
  Lines       10044    10046       +2     
==========================================
- Hits         4028     4025       -3     
- Misses       5530     5535       +5     
  Partials      486      486              

:umbrella: View full report in Codecov by Sentry.
:loudspeaker: Have feedback on the report? Share it here.

codecov[bot] avatar Jan 02 '24 11:01 codecov[bot]

@viveksahu26 please see the open comment, the function returns early if there is an error parsing the provided response without trying the other format

haydentherapper avatar Jan 23 '24 23:01 haydentherapper

@viveksahu26 please see the open comment, the function returns early if there is an error parsing the provided response without trying the other format

hey @haydentherapper tried and updated the code. One more thing should I add test for different scenarios, like:

  • when rekor-bundle comes nil
  • when unmarshaling throws an errors.

viveksahu26 avatar Jan 30 '24 04:01 viveksahu26

I agree with the points you discussed, especially the output of curl https://rekor.sigstore.dev/api/v1/log/entries?logIndex=60606559 to consider as rekor-response and let cosign format rekor-response accordingly and after properly formatted rekor-response, cosign attach command will attach the formatted rekor-response to an image corresponding to field "dev.sigstore.cosign/bundle". Now, again this bundle in "dev.sigstore.cosign/bundle" present in image layer, whose output is equivalent to formatted rekor-response, because both have all field same . In actual the real bundle(not this one "dev.sigstore.cosign/bundle") has more field in it than rekor-response such as base64Signature, cert and rekor-response. So, calling "dev.sigstore.cosign/bundle" make no sense, but which to be rekor-response will makes more sense. That's why I am also interested to know what output will cosign sign return for bundle, but sadly this doesn't support currently. Otherwise it would have been more clearly visible that whether it return "dev.sigstore.cosign/bundle" or actual real bundle. Whereas cosign sign-blob return bundle output which consist of fields: base64Signature, cert and formatted rekor-response.

ATM, we should replace this dev.sigstore.cosign/bundle" as dev.sigstore.cosign/rekor-response".

Actual rekor-response would look like:

curl https://rekor.sigstore.dev/api/v1/log/entries\?logIndex\=67934953 | jq                                          
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  2931    0  2931    0     0   6572      0 --:--:-- --:--:-- --:--:--  6586
{
  "24296fb24b8ad77a8ada322cbba201d23b88acd2f68b29e358668e3aef36306ddb2c253ca0dc9ede": {
    "body": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI3YTIxODlmNzNlYTVkYmVlMmQ1NjgxMGU4NjBjZDgxNjdlYTFiMGYzMTdkZGRjMmU0YzE2NmU3ZDY4NzUzOGYwIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FUUNJQlZtMVc1YlNSOXBTYVFyRWRVL2dOeEZuaVQzMCs4RGp5SG9naERwWHM3b0FpQXMyby82c3l2bzRaTDd3YVUrMXBNeVIvR1dNWlZTaUdzRm5hSm04ZDZZZ0E9PSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCUVZVSk1TVU1nUzBWWkxTMHRMUzBLVFVacmQwVjNXVWhMYjFwSmVtb3dRMEZSV1VsTGIxcEplbW93UkVGUlkwUlJaMEZGUldGSWNHUmhZVE13ZEVOaVMxbG5lalpyUlhsQmNqTXJORmh5TWdwb1pWUjNaM2hQVVVrM2QwcDRiVWhIWW5OU1dYcEJWbFJyUVN0RGIzVjZUblZrTUc5eGRuUTRXVXB0Tm5CQlFUZG5SbUZFUkZGU05EUlJQVDBLTFMwdExTMUZUa1FnVUZWQ1RFbERJRXRGV1MwdExTMHRDZz09In19fX0=",
    "integratedTime": 1706680021,
    "logID": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d",
    "logIndex": 67934953,
    "verification": {
      "inclusionProof": {
        "checkpoint": "rekor.sigstore.dev - 2605736670972794746\n63781918\nHfsAZNXTujUoN6lWTZS92XQqF2k0Hdrc6YZ4Onnc5kI=\nTimestamp: 1706681910431084977\n\n— rekor.sigstore.dev wNI9ajBEAiBAw+qvNLfEP0qj18Vywbp4iuuj4LKcvb9PxlYnVCFt5AIgS/+5j2fjr7ei6shGHm8cjPSVFCYzlKgMHicvxubYJyU=\n",
        "hashes": [
          "fad7073c0b8b3d73dd61eadad6c1fe267b7caa6efef581af9d4d57728681e6b3",
          "11a7c8eb98ad5ba52233abed26f320744082c4c8f68daf82d5f609264c067669",
          "749c9ca1d2b12751d1ed5916e3e79f56bc67bffa28d7a1453421677b1e364485",
          "76d989c0400a50765b1d8c86bc05a64374e2580dd4ccb9439b43c8d40056b6c3",
          "c392d68963244c9e2b6ee8c1e86e4752b6bf2c0b75825302eabc2baec6c328f7",
          "038dbf97ce93a2808c480ac7aa15bb727c9f1109ccd13b5ba75559db260e65a7",
          "21a92612613df90299ba7ca6fbd47b9fd528b78518422763fb9049885ab89a05",
          "8970a820614ded0ab40a27c891f3e3a1142304342972cfc534e7d121aa4fa3e1",
          "87eb630e892d862a2a6b893c180006bf19de9ab7f1525991ae479c5844be3d10",
          "1f762e7f648d56546b855e8d77b93071a3bba25b576f067b4bbf6164e43a67a0",
          "08383fd08e0d3b80138d8e6ca4ab2d685ea79da1b805ce7a50f0ad3c59b0b30a",
          "e3d9a82d92fb0df4d4cf59441893a35e4df510e37ae23eca86f2091926048fbb",
          "3cda1a514a77dbdb460349a34ba4f755dcd85a755ae338e5033f6e5572eb6307",
          "6c2db1d0574f019b34a46280cf3f38ce2a4937ea816da7efad192672fcde9b29",
          "f782ddd94e6b75d4cede1132c7203e13ad793e5bff80630d244f642e29adccde",
          "5d8b1ca6301b537730a86e4083ffb05534bba7c08ef38be053b7fef5f47d91ee",
          "cd0d9f7bcf79e949fefbac0572994b304f73e626394164f14164abb550a6c9a3",
          "74f801e4996a8332bfc30de5a49f1256da593c09a7f5b94f3677df835b6742a5",
          "51e5d80682cc50abdb392ed3a0cb1aa1b946e1f4bff103d04d314620155e13bd",
          "98c486feb5d87092a78a46c4b5be04868654900affc2e86ffb20074dc73a883a",
          "6969c49bd73f19bf28a5eaeabd331ddd60502defb2cd3d96e17b741c80adec6c"
        ],
        "logIndex": 63771522,
        "rootHash": "1dfb0064d5d3ba352837a9564d94bdd9742a1769341ddadce986783a79dce642",
        "treeSize": 63781918
      },
      "signedEntryTimestamp": "MEUCIFKfSMY/DU+jTzgnZCC52Id+5TYi87eCKjnjQF7EG7CXAiEA0CI+zIfoOGKrGtKhMSZ3rPb7VVPXrwk8n9paqsJFI3Y="
    }
  }
}

Formatted rekor-response would be same as rekor-response but remove verification field from it:

{   
    "body": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI3YTIxODlmNzNlYTVkYmVlMmQ1NjgxMGU4NjBjZDgxNjdlYTFiMGYzMTdkZGRjMmU0YzE2NmU3ZDY4NzUzOGYwIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FUUNJQlZtMVc1YlNSOXBTYVFyRWRVL2dOeEZuaVQzMCs4RGp5SG9naERwWHM3b0FpQXMyby82c3l2bzRaTDd3YVUrMXBNeVIvR1dNWlZTaUdzRm5hSm04ZDZZZ0E9PSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCUVZVSk1TVU1nUzBWWkxTMHRMUzBLVFVacmQwVjNXVWhMYjFwSmVtb3dRMEZSV1VsTGIxcEplbW93UkVGUlkwUlJaMEZGUldGSWNHUmhZVE13ZEVOaVMxbG5lalpyUlhsQmNqTXJORmh5TWdwb1pWUjNaM2hQVVVrM2QwcDRiVWhIWW5OU1dYcEJWbFJyUVN0RGIzVjZUblZrTUc5eGRuUTRXVXB0Tm5CQlFUZG5SbUZFUkZGU05EUlJQVDBLTFMwdExTMUZUa1FnVUZWQ1RFbERJRXRGV1MwdExTMHRDZz09In19fX0=",
    "integratedTime": 1706680021,
    "logID": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d",
    "logIndex": 67934953,
     "signedEntryTimestamp": "MEUCIFKfSMY/DU+jTzgnZCC52Id+5TYi87eCKjnjQF7EG7CXAiEA0CI+zIfoOGKrGtKhMSZ3rPb7VVPXrwk8n9paqsJFI3Y="
    }
}

Output of dev.sigstore.cosign/bundle:

  "SignedEntryTimestamp": "MEUCIBLPDfNvAm2Rw446aiM/E37l7iAWCBu58PrxZIv5TmLYAiEAufaHvGAB3zZPlykdT+HSkLPNiO62OUEIIby9/4E7MCY=",
  "Payload": {
    "body": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJjOTA5YWVlYmQ5MzU3YzA4MGE3N2VhOWQzZWVmMzU1ODMwYTMxNTRjNTJmODlhOWJlMDUyMDMxN2ZlNTQ1NjdhIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FWUNJUUNrWHBBUUlpYlVpYkVaQ2FkSWptclBXbDdLMlpiTmdrdUtXU01WVkl6RkZRSWhBSkE5UHZJb2RGcythMmFWSGtBMFNDVlYvbVBPdG1jZlN0MTQ2ZU1lSjFiRyIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCUVZVSk1TVU1nUzBWWkxTMHRMUzBLVFVacmQwVjNXVWhMYjFwSmVtb3dRMEZSV1VsTGIxcEplbW93UkVGUlkwUlJaMEZGVmpkS2NERk9RbXh1TmxWWWFqSk5jVE5YTTJKRk1XRnRiM3BwVVFwUWJrbzBSVmg0TlVsaGExUndiMVp1WW1aWFZrazRZVXRNYTBJclQzTmxORUowZFRrNVVuVk5Ra2R1YjJVd1ZWRnZkelJPZEZwSUswTlJQVDBLTFMwdExTMUZUa1FnVUZWQ1RFbERJRXRGV1MwdExTMHRDZz09In19fX0=",
    "integratedTime": 1706459446,
    "logIndex": 67201951,
    "logID": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d"
  }
}

QUESTION: If rekor-response has no payload field, then why to use payload field in dev.sigstore.cosign/bundle ??

Output of bundle retrieve from cosign sign-blob o/p:

{
  "base64Signature": "MEQCIBVm1W5bSR9pSaQrEdU/gNxFniT30+8DjyHoghDpXs7oAiAs2o/6syvo4ZL7waU+1pMyR/GWMZVSiGsFnaJm8d6YgA==",
  "rekorBundle": {
    "SignedEntryTimestamp": "MEUCIQDa3gvz9sKc3WCl1fMqdV6bT2MmeS7k3mPxAkN+UwuAgwIgLDdm0QELkfmMbdMpDCBnuASyPrdJuU/65Iiuq3T8EAU=",
    "Payload": {
      "body": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI3YTIxODlmNzNlYTVkYmVlMmQ1NjgxMGU4NjBjZDgxNjdlYTFiMGYzMTdkZGRjMmU0YzE2NmU3ZDY4NzUzOGYwIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FUUNJQlZtMVc1YlNSOXBTYVFyRWRVL2dOeEZuaVQzMCs4RGp5SG9naERwWHM3b0FpQXMyby82c3l2bzRaTDd3YVUrMXBNeVIvR1dNWlZTaUdzRm5hSm04ZDZZZ0E9PSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCUVZVSk1TVU1nUzBWWkxTMHRMUzBLVFVacmQwVjNXVWhMYjFwSmVtb3dRMEZSV1VsTGIxcEplbW93UkVGUlkwUlJaMEZGUldGSWNHUmhZVE13ZEVOaVMxbG5lalpyUlhsQmNqTXJORmh5TWdwb1pWUjNaM2hQVVVrM2QwcDRiVWhIWW5OU1dYcEJWbFJyUVN0RGIzVjZUblZrTUc5eGRuUTRXVXB0Tm5CQlFUZG5SbUZFUkZGU05EUlJQVDBLTFMwdExTMUZUa1FnVUZWQ1RFbERJRXRGV1MwdExTMHRDZz09In19fX0=",
      "integratedTime": 1706680021,
      "logIndex": 67934953,
      "logID": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d"
    }
  }
}

viveksahu26 avatar Jan 31 '24 09:01 viveksahu26

@haydentherapper any thought on this ?

viveksahu26 avatar Feb 05 '24 08:02 viveksahu26

Hey sorry I will respond early next week! I have some ideas on how to simplify this

haydentherapper avatar Feb 09 '24 23:02 haydentherapper