Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Sigstore signing and verification of collections in Ansible repositories (refactored) #1428

Closed

Conversation

mayaCostantini
Copy link
Contributor

@mayaCostantini mayaCostantini commented Apr 18, 2023

Opening this PR as a refactored version of #1390

TODO (@mayaCostantini ):

@mayaCostantini mayaCostantini marked this pull request as draft April 18, 2023 14:03
@@ -275,6 +318,381 @@ class Meta:
unique_together = ("pubkey_fingerprint", "signed_collection")



class SigstoreSigningService(Content):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not be content.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would be a better model to subclass?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SigningService or BaseModel

fulcio_url = models.TextField(default=PUBLIC_FULCIO_URL)
tuf_url = models.TextField(default=PUBLIC_TUF_URL)
oidc_issuer = models.TextField(default=PUBLIC_ISSUER_URL)
credentials_file_path = models.TextField(null=True)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important note: this field is just here temporarily to test the signing workflow by retrieving the OIDC credentials from a file stored in plaintext on the server. This is not the approach we will be going for; I will replace it once an alternative flow to get credentials has been validated.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note we have an encrypted field that we use for storing passwords in Pulp. You still might not want to go down this route, but it is an option: https://github.com/pulp/pulpcore/blob/main/pulpcore/app/models/fields.py#L79-L80

@mdellweg
Copy link
Member

Can you please elaborate on who is signing what (ansible collections, i guess), and what it means for a content to have a valid signature?

Comment on lines +119 to +109
if "from_file" in options:
file_path = Path(options["from_file"])

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this command only available when using a config file? I don't understand what this if is checking for.

pulp_ansible/app/models.py Outdated Show resolved Hide resolved
fulcio_url = models.TextField(default=PUBLIC_FULCIO_URL)
tuf_url = models.TextField(default=PUBLIC_TUF_URL)
oidc_issuer = models.TextField(default=PUBLIC_ISSUER_URL)
credentials_file_path = models.TextField(null=True)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note we have an encrypted field that we use for storing passwords in Pulp. You still might not want to go down this route, but it is an option: https://github.com/pulp/pulpcore/blob/main/pulpcore/app/models/fields.py#L79-L80

pulp_ansible/app/serializers.py Outdated Show resolved Hide resolved
pulp_ansible/app/sigstore/tasks/sigstore_signature.py Outdated Show resolved Hide resolved
pulp_ansible/app/sigstore/tasks/sigstore_signature.py Outdated Show resolved Hide resolved
pulp_ansible/app/sigstore/tasks/sigstore_signature.py Outdated Show resolved Hide resolved
Comment on lines 897 to 901
def sigstore_sign(self, request, pk):
"""
Dispatches a Sigstore signing task.
This endpoint is in tech preview and can change at any time in the future.
"""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to move this into the existing sign endpoint? From what I've seen it's not that different call argument wised to what we have already. If you turn the SigstoreSigningService to be of type SigningService then you can use similar logic like we have in sync and use the type of the signing service to determine which task to call.

@@ -6,3 +6,6 @@ Pillow>=7.0,<9.6
pulpcore>=3.23.0,<3.25
PyYAML>=5.4.1,<7.0
semantic_version>=2.9,<2.11
setuptools>=39.2,<66.2.0
sigstore==1.1.1
voluptuous==0.13.1
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is voluptuous still being used? Also what is setuptools being used for? I would like to keep that out the requirements.

@mayaCostantini
Copy link
Contributor Author

Thanks a lot for the review @gerrod3 @mdellweg

I tried to address most comments in my last commits, the async logic in the signing task being the only thing I didn't manage to change as you wished (I tried the approach you proposed but it was not working, so I will give it a closer look).

Feel free to give it another review round. In the meantime I will continue with the missing items on my list which are secure credential storage for the OIDC client secret, updating tests and writing the corresponding signing commands in pulp-cli and get back to you when it is ready.

@mayaCostantini mayaCostantini force-pushed the repo-sigstore-sign-verify branch from 0577fa4 to 88e2cb8 Compare May 31, 2023 10:47
Copy link
Contributor

@gerrod3 gerrod3 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the late review, lots of things still to discuss.

PUBLIC_OIDC_ISSUERS = (
"https://accounts.google.com",
"https://oauth2.sigstore.dev/auth",
"http://dex-idp:8888/auth",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a real legit link? Looks weird, especially sus since it is not https.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it is stated in the official Fulcio config, probably for a locally deployed Dex server.


name = models.CharField(db_index=True, unique=True, max_length=64)
rekor_url = models.TextField(default=PUBLIC_REKOR_URL)
rekor_root_pubkey = models.TextField(null=True)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the default rekor_url is going to be the public url, should this rekor_root_pubkey also default to the public root pubkey? Maybe this should be accounted for in the create command.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't put a default here because the privileged way of retrieving Sigstore instances public keys should be via TUF, the public key fields for Rekor and the CTLog being here just in case users need to override key retrieval by TUF.

fulcio_url = models.TextField(default=PUBLIC_FULCIO_URL)
tuf_url = models.TextField(default=PUBLIC_TUF_URL, null=True)
oidc_issuer = models.TextField(default=PUBLIC_ISSUER_URL)
oidc_client_secret = EncryptedTextField(null=True)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we really allow this to be null? Can you sign something without being identified, or is this tied to the enabled_interactive field?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this could be null in case users want to authenticate interactively. But in general, I would be open to postponing the implementation of interactive signing as it poses a lot of challenges regarding Pulp tasking and needs to be implemented in the Automation Hub / Galaxy UI as well. Wdyt? Should this be planned in a future version instead?

pulp_ansible/app/models.py Outdated Show resolved Hide resolved
pulp_ansible/app/models.py Outdated Show resolved Hide resolved
Comment on lines 117 to 120
with tempfile.NamedTemporaryFile(dir=".", delete=False, mode="w") as manifest_file:
manifest_file.write(checksums)
manifest_file.flush()
with open(manifest_file.name, "rb", buffering=0) as manifest_io:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since checksums is already in-memory no need to re-write it to a file, use io.BytesIO instead: https://docs.python.org/3/library/io.html#binary-i-o

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in df18426


data["data"] = signature
data["sigstore_x509_certificate"] = certificate
data["sigstore_signing_service"] = sigstore_signing_service
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
data["sigstore_signing_service"] = sigstore_signing_service

Uploaded signatures don't have a signing service.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think they actually do:

{
            "pulp_href": "/pulp/api/v3/content/ansible/content/68aeb271-691b-4910-baa8-bce859929b50/",
            "pulp_created": "2023-06-14T13:06:47.645521Z",
            "data": "MGYCMQD8J17K9G0aqKZTBPBJvL97P27fDVPp1HT/wHgxvF+h4HrxIwPYsYQG9E0aYKTzHmcCMQD3ixhAUsKdrUZTsQLEySOHVKrD61CvOeCkkk02Kgj6e+NlW99mJyCjLuoKb5v28ds=",
            "sigstore_x509_certificate": "-----BEGIN CERTIFICATE-----\nMIIDsTCCA1igAwIBAgIUUqFobSm73ExZkB6R2FBe8I/hxTkwCgYIKoZIzj0EAwIw\ngZcxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJNQTEPMA0GA1UEBwwGQm9zdG9uMRAw\nDgYDVQQKDAdSZWQgSGF0MRAwDgYDVQQLDAdSZWQgSGF0MRYwFAYDVQQDDA1vcGVu\nc2hpZnRhcHBzMS4wLAYJKoZIhvcNAQkBFh9vcGVuc2hpZnRhcHBzQG9wZW5zaGlm\ndGFwcHMuY29tMB4XDTIzMDYxNDEzMDY0NloXDTIzMDYxNDEzMTY0NlowADB2MBAG\nByqGSM49AgEGBSuBBAAiA2IABJk/8FHb0PA7kU5/zbbuQoFsAd1TJj+FWB1PyGtA\nVTBK4MvuXgcOKKQcI0GLeadac0HdvfZjsNitEhjobnH8+dDqvHsHOM9w+R2aZL2K\nB8dAeD9eiw5Jpa7Sov8noA94M6OCAfkwggH1MA4GA1UdDwEB/wQEAwIHgDATBgNV\nHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUHk6ltOJgcKWkUGFpn545shIW8LIw\nHwYDVR0jBBgwFoAUwXoXJ20AjXcfuZ0Q+MfzGM5QU1cwLAYDVR0RAQH/BCIwIIEe\nbWF5YS5jb3N0YW50aW5pQHByb3Rvbm1haWwuY29tMGgGCisGAQQBg78wAQEEWmh0\ndHBzOi8va2V5Y2xvYWsta2V5Y2xvYWsuYXBwcy5vcGVuLXN2Yy1zdHMuazF3bC5w\nMS5vcGVuc2hpZnRhcHBzLmNvbS9hdXRoL3JlYWxtcy9zaWdzdG9yZTBqBgorBgEE\nAYO/MAEIBFwMWmh0dHBzOi8va2V5Y2xvYWsta2V5Y2xvYWsuYXBwcy5vcGVuLXN2\nYy1zdHMuazF3bC5wMS5vcGVuc2hpZnRhcHBzLmNvbS9hdXRoL3JlYWxtcy9zaWdz\ndG9yZTCBiQYKKwYBBAHWeQIEAgR7BHkAdwB1APlNklTuzJCFaZy/xxsoT1OgbYHg\nht07hNnZZSKIPp7GAAABiLoECfUAAAQDAEYwRAIgNuwoktic/YuF8VCSKnlhgZM4\nfxzvWtveNKTFoFaOqJ0CID6wdRqeoYJTNfgABjJyraiSRQGaa4GAf39pRNsOF3yg\nMAoGCCqGSM49BAMCA0cAMEQCIEgamVx2WHUSb7v562wGHA+4DHh+1B/6HawbIIox\n3XfgAiAG3+taNA4j8oJIiNWDNrkMbHl4eWy3YZrWDQN2SO8lTw==\n-----END CERTIFICATE-----\n",
            "sigstore_bundle": "{\"mediaType\": \"application/vnd.dev.sigstore.bundle+json;version=0.1\", \"verificationMaterial\": {\"x509CertificateChain\": {\"certificates\": [{\"rawBytes\": \"MIIDsTCCA1igAwIBAgIUUqFobSm73ExZkB6R2FBe8I/hxTkwCgYIKoZIzj0EAwIwgZcxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJNQTEPMA0GA1UEBwwGQm9zdG9uMRAwDgYDVQQKDAdSZWQgSGF0MRAwDgYDVQQLDAdSZWQgSGF0MRYwFAYDVQQDDA1vcGVuc2hpZnRhcHBzMS4wLAYJKoZIhvcNAQkBFh9vcGVuc2hpZnRhcHBzQG9wZW5zaGlmdGFwcHMuY29tMB4XDTIzMDYxNDEzMDY0NloXDTIzMDYxNDEzMTY0NlowADB2MBAGByqGSM49AgEGBSuBBAAiA2IABJk/8FHb0PA7kU5/zbbuQoFsAd1TJj+FWB1PyGtAVTBK4MvuXgcOKKQcI0GLeadac0HdvfZjsNitEhjobnH8+dDqvHsHOM9w+R2aZL2KB8dAeD9eiw5Jpa7Sov8noA94M6OCAfkwggH1MA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUHk6ltOJgcKWkUGFpn545shIW8LIwHwYDVR0jBBgwFoAUwXoXJ20AjXcfuZ0Q+MfzGM5QU1cwLAYDVR0RAQH/BCIwIIEebWF5YS5jb3N0YW50aW5pQHByb3Rvbm1haWwuY29tMGgGCisGAQQBg78wAQEEWmh0dHBzOi8va2V5Y2xvYWsta2V5Y2xvYWsuYXBwcy5vcGVuLXN2Yy1zdHMuazF3bC5wMS5vcGVuc2hpZnRhcHBzLmNvbS9hdXRoL3JlYWxtcy9zaWdzdG9yZTBqBgorBgEEAYO/MAEIBFwMWmh0dHBzOi8va2V5Y2xvYWsta2V5Y2xvYWsuYXBwcy5vcGVuLXN2Yy1zdHMuazF3bC5wMS5vcGVuc2hpZnRhcHBzLmNvbS9hdXRoL3JlYWxtcy9zaWdzdG9yZTCBiQYKKwYBBAHWeQIEAgR7BHkAdwB1APlNklTuzJCFaZy/xxsoT1OgbYHght07hNnZZSKIPp7GAAABiLoECfUAAAQDAEYwRAIgNuwoktic/YuF8VCSKnlhgZM4fxzvWtveNKTFoFaOqJ0CID6wdRqeoYJTNfgABjJyraiSRQGaa4GAf39pRNsOF3ygMAoGCCqGSM49BAMCA0cAMEQCIEgamVx2WHUSb7v562wGHA+4DHh+1B/6HawbIIox3XfgAiAG3+taNA4j8oJIiNWDNrkMbHl4eWy3YZrWDQN2SO8lTw==\"}]}, \"tlogEntries\": [{\"logIndex\": \"349\", \"logId\": {\"keyId\": \"IFajaKdLy+sgq4+XSC5BH0KbPN8Kpcq7tYo5QMdCBpQ=\"}, \"kindVersion\": {\"kind\": \"hashedrekord\", \"version\": \"0.0.1\"}, \"integratedTime\": \"1686748007\", \"inclusionPromise\": {\"signedEntryTimestamp\": \"MEYCIQCVdYM7DhTo4ZQguWbXQD+R73UynZFtadNYjRATyBdLWwIhANPrpndVfmIf55c5Bn+dlCBGgO/IbEu8RWnxnK48jnd8\"}, \"inclusionProof\": {\"logIndex\": \"349\", \"rootHash\": \"pfsRcGU1+YTDFU2FtCoc7Ig8iv7nxypYzVn35KlIiSQ=\", \"treeSize\": \"350\", \"hashes\": [\"2Ln+aSDCopdrk9aY+DfGrwgKmkbWZuxb34NX4nCSQIg=\", \"JNyCbro9UJ9Yfhv55KCJ+vQQgv8029bretUa0ZAWFws=\", \"i+cLXIsbD4zB+OnN4JqNSuSajScXnV+UZ7B3H2o2azQ=\", \"54cKDlSFbb1IGAJLeE3wt6rWuhW4Mo7Dvu3y+UqeCwY=\", \"GUEiTpxawkRVsubS/3/DkEjy3qVFIqxwfafGliKkDTM=\", \"SfXoge+ONFcKFoRk3bbplo7ucOU9hE177NQdZxxvWZs=\"]}, \"canonicalizedBody\": \"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1In19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1HWUNNUUQ4SjE3SzlHMGFxS1pUQlBCSnZMOTdQMjdmRFZQcDFIVC93SGd4dkYraDRIcnhJd1BZc1lRRzlFMGFZS1R6SG1jQ01RRDNpeGhBVXNLZHJVWlRzUUxFeVNPSFZLckQ2MUN2T2VDa2trMDJLZ2o2ZStObFc5OW1KeUNqTHVvS2I1djI4ZHM9IiwicHVibGljS2V5Ijp7ImNvbnRlbnQiOiJMUzB0TFMxQ1JVZEpUaUJEUlZKVVNVWkpRMEZVUlMwdExTMHRDazFKU1VSelZFTkRRVEZwWjBGM1NVSkJaMGxWVlhGR2IySlRiVGN6UlhoYWEwSTJVakpHUW1VNFNTOW9lRlJyZDBObldVbExiMXBKZW1vd1JVRjNTWGNLWjFwamVFTjZRVXBDWjA1V1FrRlpWRUZzVmxSTlVYTjNRMUZaUkZaUlVVbEVRVXBPVVZSRlVFMUJNRWRCTVZWRlFuZDNSMUZ0T1hwa1J6bDFUVkpCZHdwRVoxbEVWbEZSUzBSQlpGTmFWMUZuVTBkR01FMVNRWGRFWjFsRVZsRlJURVJCWkZOYVYxRm5VMGRHTUUxU1dYZEdRVmxFVmxGUlJFUkJNWFpqUjFaMUNtTXlhSEJhYmxKb1kwaENlazFUTkhkTVFWbEtTMjlhU1doMlkwNUJVV3RDUm1nNWRtTkhWblZqTW1od1dtNVNhR05JUW5wUlJ6bDNXbGMxZW1GSGJHMEtaRWRHZDJOSVRYVlpNamwwVFVJMFdFUlVTWHBOUkZsNFRrUkZlazFFV1RCT2JHOVlSRlJKZWsxRVdYaE9SRVY2VFZSWk1FNXNiM2RCUkVJeVRVSkJSd3BDZVhGSFUwMDBPVUZuUlVkQ1UzVkNRa0ZCYVVFeVNVRkNTbXN2T0VaSVlqQlFRVGRyVlRVdmVtSmlkVkZ2Um5OQlpERlVTbW9yUmxkQ01WQjVSM1JCQ2xaVVFrczBUWFoxV0dkalQwdExVV05KTUVkTVpXRmtZV013U0dSMlpscHFjMDVwZEVWb2FtOWlia2c0SzJSRWNYWkljMGhQVFRsM0sxSXlZVnBNTWtzS1FqaGtRV1ZFT1dWcGR6VktjR0UzVTI5Mk9HNXZRVGswVFRaUFEwRm1hM2RuWjBneFRVRTBSMEV4VldSRWQwVkNMM2RSUlVGM1NVaG5SRUZVUW1kT1ZncElVMVZGUkVSQlMwSm5aM0pDWjBWR1FsRmpSRUY2UVdSQ1owNVdTRkUwUlVablVWVklhelpzZEU5S1oyTkxWMnRWUjBad2JqVTBOWE5vU1ZjNFRFbDNDa2gzV1VSV1VqQnFRa0puZDBadlFWVjNXRzlZU2pJd1FXcFlZMloxV2pCUkswMW1la2ROTlZGVk1XTjNURUZaUkZaU01GSkJVVWd2UWtOSmQwbEpSV1VLWWxkR05WbFROV3BpTTA0d1dWYzFNR0ZYTlhCUlNFSjVZak5TZG1KdE1XaGhWM2QxV1RJNWRFMUhaMGREYVhOSFFWRlJRbWMzT0hkQlVVVkZWMjFvTUFwa1NFSjZUMms0ZG1FeVZqVlpNbmgyV1ZkemRHRXlWalZaTW5oMldWZHpkVmxZUW5kamVUVjJZMGRXZFV4WVRqSlplVEY2WkVoTmRXRjZSak5pUXpWM0NrMVROWFpqUjFaMVl6Sm9jRnB1VW1oalNFSjZURzFPZG1KVE9XaGtXRkp2VEROS2JGbFhlSFJqZVRsNllWZGtlbVJIT1hsYVZFSnhRbWR2Y2tKblJVVUtRVmxQTDAxQlJVbENSbmROVjIxb01HUklRbnBQYVRoMllUSldOVmt5ZUhaWlYzTjBZVEpXTlZreWVIWlpWM04xV1ZoQ2QyTjVOWFpqUjFaMVRGaE9NZ3BaZVRGNlpFaE5kV0Y2UmpOaVF6VjNUVk0xZG1OSFZuVmpNbWh3V201U2FHTklRbnBNYlU1MllsTTVhR1JZVW05TU0wcHNXVmQ0ZEdONU9YcGhWMlI2Q21SSE9YbGFWRU5DYVZGWlMwdDNXVUpDUVVoWFpWRkpSVUZuVWpkQ1NHdEJaSGRDTVVGUWJFNXJiRlIxZWtwRFJtRmFlUzk0ZUhOdlZERlBaMkpaU0djS2FIUXdOMmhPYmxwYVUwdEpVSEEzUjBGQlFVSnBURzlGUTJaVlFVRkJVVVJCUlZsM1VrRkpaMDUxZDI5cmRHbGpMMWwxUmpoV1ExTkxibXhvWjFwTk5BcG1lSHAyVjNSMlpVNUxWRVp2Um1GUGNVb3dRMGxFTm5ka1VuRmxiMWxLVkU1bVowRkNha3A1Y21GcFUxSlJSMkZoTkVkQlpqTTVjRkpPYzA5R00zbG5DazFCYjBkRFEzRkhVMDAwT1VKQlRVTkJNR05CVFVWUlEwbEZaMkZ0Vm5neVYwaFZVMkkzZGpVMk1uZEhTRUVyTkVSSWFDc3hRaTgyU0dGM1lrbEpiM2dLTTFobVowRnBRVWN6SzNSaFRrRTBhamh2U2tscFRsZEVUbkpyVFdKSWJEUmxWM2t6V1ZweVYwUlJUakpUVHpoc1ZIYzlQUW90TFMwdExVVk9SQ0JEUlZKVVNVWkpRMEZVUlMwdExTMHRDZz09In19fX0=\"}]}, \"messageSignature\": {\"messageDigest\": {\"algorithm\": \"SHA2_256\", \"digest\": \"47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=\"}, \"signature\": \"MGYCMQD8J17K9G0aqKZTBPBJvL97P27fDVPp1HT/wHgxvF+h4HrxIwPYsYQG9E0aYKTzHmcCMQD3ixhAUsKdrUZTsQLEySOHVKrD61CvOeCkkk02Kgj6e+NlW99mJyCjLuoKb5v28ds=\"}}",
            "signed_collection": "/pulp/api/v3/content/ansible/collection_versions/343fc505-e15b-48b9-a3f7-897532736336/",
            "sigstore_signing_service": "/pulp/api/v3/sigstore-signing-services/5f484e77-f5b0-4916-9680-e3bf45822804/"
},

The sigstore_signing_service field is a foreign key for CollectionVersionSigstoreSignature

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why should the created signature have a reference to the signing service at all? What's the benefit of that link?
Also you can upload a signature into a system that has no signing service configured at all. So no, they don't have one.

Copy link
Contributor

@gerrod3 gerrod3 Jun 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The signatures having a reference to the signing service was originally a Galaxy request to know if they were locally created. Synced and uploaded signatures will not have an attached signing service, but I don't think they expose the upload signature endpoint in galaxy, so it actually determines if the signature was synced or locally created and can be easily used when filtering signatures.

Comment on lines 232 to 245
async with aiofiles.tempfile.NamedTemporaryFile(
dir=".", mode="w", delete=False
) as manifest_file:
manifest_data = await sync_to_async(_generate_checksum_manifest)(
collection_version
)
await manifest_file.write(manifest_data)
async with aiofiles.open(manifest_file.name, mode="rb", buffering=0) as iofile:
async with aiofiles.tempfile.NamedTemporaryFile(
dir=".", mode="w", delete=False
) as manifest_content:
content = await iofile.read()
manifest_content.write(content)
with open(manifest_content.name, "rb") as manifest_bytes:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same two things as above with the verify code:

  1. Can we just extract the MANIFEST.IN file instead of regenerating it?
  2. Once we have the file in memory let's use io.BytesIO instead of rewriting it to a temp file.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I realized _generate_checksum_manifest generates a string an not a stream of bytes, so I think writing to a file is necessary to make the conversion here, wdyt? I might revert df18426 in this case

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just use TextIo instead.

pulp_ansible/app/tasks/signature.py Outdated Show resolved Hide resolved
Comment on lines 62 to 64
with _OAuthFlow(client_id, client_secret, self) as server:
# Launch web browser
if webbrowser.open(server.base_uri):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code simply won't work for the signing task as 99% of the time the machine the task is running on is not accessible by the user, so the signing task will simply stall until it fails waiting for the user input.

I think I have a good idea for how to get around this problem, but it's pretty in-depth and will take me a while to write down. I would also like to get feedback from my other team-members, so let's discuss it at our next sync.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your input during the sync, I removed the interactive signing part in ca80190

@mayaCostantini mayaCostantini force-pushed the repo-sigstore-sign-verify branch from 39bffb7 to 18bb3b4 Compare June 13, 2023 09:39
@mayaCostantini
Copy link
Contributor Author

mayaCostantini commented Jun 14, 2023

Thanks again for the thorough review @gerrod3
I tried to answer most of the comments you raised, please let me know if that works for you.

As discussed during the last sync, I will start a refactor based on sigstore-python v2 as the milestone seems not too far from being reached as of now: https://github.com/sigstore/sigstore-python/milestone/4

@mayaCostantini mayaCostantini force-pushed the repo-sigstore-sign-verify branch from 781dfb9 to cccfd0b Compare October 23, 2023 17:56
- `Fulcio <https://docs.sigstore.dev/fulcio/overview/>`__
is a free Certificate Authority used to generate ephemeral signing certificates for artifacts.

- `Cosign https://docs.sigstore.dev/cosign/overview/`__
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- `Cosign https://docs.sigstore.dev/cosign/overview/`__
- `Cosign <https://docs.sigstore.dev/cosign/overview/>`__

TYPE = "collection_sigstore_signatures"

sigstore_bundle = models.JSONField(null=True)
sigstore_bundle_sha256_hash = models.CharField(max_length=64, unique=True, db_index=True, null=True)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
sigstore_bundle_sha256_hash = models.CharField(max_length=64, unique=True, db_index=True, null=True)
sigstore_bundle_sha256_hash = models.CharField(max_length=64, db_index=True, null=True)

unique_together is enough. Don't forget to remove the same code from the migration file.

@@ -128,6 +139,148 @@ class Meta:
)


class SigstoreSigningServiceSerializer(ModelSerializer):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can only be created from the command line, correct? If so then I would change all the fields to read_only=True.

)


class SigstoreVerifyingServiceSerializer(ModelSerializer):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here as above.

artifact_name = artifact.file.name
artifact_file = storage.open(artifact_name)

with tempfile.TemporaryDirectory() as tempdir:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
with tempfile.TemporaryDirectory() as tempdir:
with tempfile.TemporaryDirectory(dir=".") as tempdir:

Comment on lines +60 to +61
with tempfile.TemporaryDirectory() as tempdir:
differ = DistlibManifestChecksumFileExistenceDiffer
checksum = ChecksumFile(tempdir, differ=differ)
with tarfile.open(fileobj=artifact_file, mode="r") as tar:
tar.extractall(path=tempdir)

try:
manifest = checksum.generate_gnu_style()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the status of ansible-sign? I see it below as a new dependency. Do we still need to have this code or can we call ansible-sign's code instead?

sigstore_bundle = data.get("sigstore_bundle")
checksums = _generate_checksum_manifest(collection)

bundle = Bundle().from_json(sigstore_bundle) if sigstore_bundle != "null" else None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the status of this now that V2 is out? Can this verification code be dramatically simplified? Can the verification service be removed entirely and be replaced with a text field like GPG?



class SigstoreSigningServiceViewSet(
NamedModelViewSet, mixins.RetrieveModelMixin, mixins.ListModelMixin, RolesMixin
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
NamedModelViewSet, mixins.RetrieveModelMixin, mixins.ListModelMixin, RolesMixin
NamedModelViewSet, mixins.RetrieveModelMixin, mixins.ListModelMixin

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding ansible-sign, I am still waiting for the sigstore code to be merged there (see ansible/ansible-sign#46), but the code should indeed be replaced when this is done. Let me get back to the ansible-sign maintainers and I will replace it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The verification service would still be required as policies for verification must be specified with sigstore (i.e. signer's identity, OIDC issuer, etc).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think, I said this before. There is no need for a verification service.
The design of the signing service is to protect secrets. Verification must always work without secrets. Otherwise your cryptosystem is broken. No secrets means: Everything lives in the database and is configurable via the api on the very Repository or Remote supposed to protect itself from improperly signed content.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no need for any secret here, the parameters specified are public and are simple text fields used by the sigstore client to verify the signature, exactly like you would use a public GPG key.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the term "verification service" might be misused here. It is actually more of a static configuration than a "service".

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That I'm fine with. No files stored away on the workers harddrive, no bits protected by a Hardware Crypto Token. If it's more complex than a simple text field, we can even put it in a jsonfield. But it must be transparently accessible to the user.



class SigstoreVerifyingServiceViewSet(
NamedModelViewSet, mixins.RetrieveModelMixin, mixins.ListModelMixin, RolesMixin
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
NamedModelViewSet, mixins.RetrieveModelMixin, mixins.ListModelMixin, RolesMixin
NamedModelViewSet, mixins.RetrieveModelMixin, mixins.ListModelMixin

requirements.txt Outdated
galaxy_importer>=0.4.5,<0.5
GitPython>=3.1.24,<3.2
jsonschema>=4.9,<4.20
Pillow>=7.0,<10.2
pulpcore>=3.25.0,<3.40
PyYAML>=5.4.1,<7.0
semantic_version>=2.9,<2.11
setuptools>=39.2,<66.2.0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is setuptools needed for?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure anymore why I added it here, so removing it.

@mayaCostantini
Copy link
Contributor Author

Thanks a lot for the review @gerrod3 ! I pushed the changes you requested, let me know if anything else comes to your mind.

Copy link
Contributor

@gerrod3 gerrod3 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have some more big refactoring asks. Also, I am curious about how far along ansible-sign is and if we can use it in our verification code.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reading over Matthias's comments, what do you think about removing the verifying service in place of a json field on the repository? The main issue is that the verifying service can not be a management command, it needs to be created/edited through the api by non-admin users. So the question becomes whether we want to have it be a separate model in the database or have it crafted through a json field on the repository. Some questions to help us decide:

  1. How do users set up their sigstore tooling for verifying signatures. Is their a special config file? Can we simply have users upload their file to their repository like we do for GPG?
  2. How useful is having re-usuable/shared verifying services that can be used across many repositories?
  3. How often do we expect users to update or add new verifying services. Is it a one and done, or something that can be expected to change each week?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it needs to be created/edited through the api by non-admin users.

I am not sure why the verifying service should be configurable by non-admin users, as its role is to filter collection versions that do not have valid signature according to a pre-defined policy, which is critical from a security point of view.
Regarding having the verifying service replaced by a JSON field, I have nothing against it except if this implies any non-admin user can configure it.

To answer your questions:

  1. If users use a CLI such as ansible-galaxy to pull collections from a repository, they will use the command options to specify the verification policies (for example, OIDC issuer and signer's identity). The same is true for tooling from automated workflows (github actions, etc).
  2. It would probably be useful, as private content in an organizations is likely to originate from the same few sources most of the time (however, this is just an assumption).
  3. It would be more of a one and done or once-in-a-while action. As mentioned above, content signature sources are not so likely to change on a frequent basis.

Comment on lines +837 to +838
for sigstore_verifying_service in sigstore_verifying_services:
verification_result = sigstore_verifying_service.sigstore_verify(manifest=io, sigstore_bundle=sigstore_bundle)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should check this setting: settings.ANSIBLE_SIGNATURE_REQUIRE_VERIFICATION and if it fails all verification with this setting then we raise the failure, else just don't create the signature.

return manifest


def verify_sigstore_signature_upload(data):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think just like its counterpart, verify_signature_upload, this method should use the same logic with settings.ANSIBLE_SIGNATURE_REQUIRE_VERIFICATION and emit a warning log if there are no verifying services on the repository.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 3b27be9

Comment on lines 180 to 181
async def sigstore_sign_collection_versions(self, collection_versions):
"""Signs the collection versions with Sigstore."""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
async def sigstore_sign_collection_versions(self, collection_versions):
"""Signs the collection versions with Sigstore."""
async def sigstore_sign_collection_version(self, collection_version):
"""Signs the collection version with Sigstore."""

This should be refactored to handle just one collection version to better take advantage of async code.

Comment on lines 183 to 189
client_secret = self.sigstore_signing_service.oidc_client_secret

identity = self.sigstore_signing_service.issuer.identity_token(
"sigstore", client_secret,
)

signing_ctx = self.sigstore_signing_service.signing_context
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would move this into init.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in e0218f0

with signing_ctx.signer(identity) as signer:
# Limits the number of subprocesses spawned/running at one time
async with self.semaphore:
async for collection_version in collection_versions:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
async for collection_version in collection_versions:

Comment on lines 224 to 228
await asyncio.create_task(
self.sigstore_sign_collection_versions(
sync_to_async_iterable(new_content.iterator())
)
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
await asyncio.create_task(
self.sigstore_sign_collection_versions(
sync_to_async_iterable(new_content.iterator())
)
)
async for collection_version in sync_to_async_iterable(new_content.iterator()):
tasks.append(asyncio.create_task(self.sigstore_sign_collection_version(collection_version)))
await asyncio.gather(*tasks)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed by 6346d5d

@@ -26,7 +26,7 @@

DEFAULTS = {
"rekor-url": "https://rekor.sigstore.dev",
"tuf-url": "https://sigstore-tuf-root.storage.googleapis.com/",
"tuf-url": "https://tuf-repo-cdn.sigstore.dev/",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this going to be the final url?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is the one currently used for the public Sigstore instance

Comment on lines +412 to +413
if self.tuf_url.endswith("/"):
self.tuf_url = self.tuf_url[-1]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if self.tuf_url.endswith("/"):
self.tuf_url = self.tuf_url[-1]
if self.tuf_url.endswith("/"):
self.tuf_url = self.tuf_url[:-1]

You could also remove the 'if' statement with self.tuf_url = self.tuf_url.rstrip("/")

Comment on lines +482 to +489
@property
def get_rekor_url(self):
if self.rekor_url.endswith("/"):
self.rekor_url = self.rekor_url[:-1]
return self.rekor_url

@property
def get_tuf_url(self):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this really necessary? You didn't do the same on the SigningService model.

Comment on lines +858 to +861
elif settings.ANSIBLE_SIGNATURE_REQUIRE_VERIFICATION:
raise VerificationFailureException(
"Failed to verify Sigstore signature against all verifying services configured on this repository with ANSIBLE_SIGNATURE_REQUIRE_VERIFICATION enabled."
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure this elif statement is on the right indentation. I was expecting it to be a part of the for loop if we don't break out of it (which I think you need to add since we don't want to save the signature twice if we have a verifier).

)
async for collection_version in sync_to_async_iterable(new_content.iterator()):
tasks.append(asyncio.create_task(self.sigstore_sign_collection_version(collection_version)))
await asyncio.gather(*tasks)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
await asyncio.gather(*tasks)
await asyncio.gather(*tasks)

This needs to be outside the for loop else we would still be doing the tasks serially.

Copy link

stale bot commented Mar 5, 2024

This pull request has been marked 'stale' due to lack of recent activity. If there is no further activity, the PR will be closed in another 30 days. Thank you for your contribution!

@stale stale bot added the stale label Mar 5, 2024
Copy link

stale bot commented Apr 9, 2024

This pull request has been closed due to inactivity. If you feel this is in error, please reopen the pull request or file a new PR with the relevant details.

@stale stale bot closed this Apr 9, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants