|
1 | 1 | import logging |
2 | 2 | import os |
| 3 | +import json |
3 | 4 | from gettext import gettext as _ |
| 5 | +from cryptography import x509 |
4 | 6 | from django.conf import settings |
5 | 7 | from django.db.utils import IntegrityError |
6 | 8 | from packaging.requirements import Requirement |
7 | 9 | from rest_framework import serializers |
| 10 | +from pydantic import ValidationError |
| 11 | +from pypi_attestations import Distribution, Provenance, VerificationError |
8 | 12 |
|
9 | 13 | from pulpcore.plugin import models as core_models |
10 | 14 | from pulpcore.plugin import serializers as core_serializers |
@@ -454,6 +458,138 @@ class Meta: |
454 | 458 | model = python_models.PythonPackageContent |
455 | 459 |
|
456 | 460 |
|
| 461 | +class PackageProvenanceSerializer(core_serializers.NoArtifactContentUploadSerializer): |
| 462 | + """ |
| 463 | + A Serializer for PackageProvenance. |
| 464 | + """ |
| 465 | + |
| 466 | + package = core_serializers.DetailRelatedField( |
| 467 | + help_text=_("The package that the provenance is for."), |
| 468 | + view_name_pattern=r"content(-.*/.*)-detail", |
| 469 | + queryset=python_models.PythonPackageContent.objects.all(), |
| 470 | + ) |
| 471 | + provenance = serializers.JSONField(read_only=True, default=dict) |
| 472 | + sha256 = serializers.CharField(read_only=True) |
| 473 | + verify = serializers.BooleanField( |
| 474 | + default=True, |
| 475 | + write_only=True, |
| 476 | + help_text=_("Verify each attestation in the provenance."), |
| 477 | + ) |
| 478 | + |
| 479 | + def deferred_validate(self, data): |
| 480 | + """ |
| 481 | + Validate that the provenance is valid and pointing to the correct package. |
| 482 | + """ |
| 483 | + data = super().deferred_validate(data) |
| 484 | + try: |
| 485 | + provenance = Provenance.model_validate_json(data["file"].read()) |
| 486 | + data["provenance"] = provenance.model_dump(mode="json") |
| 487 | + except ValidationError as e: |
| 488 | + raise serializers.ValidationError( |
| 489 | + _("The uploaded provenance is not valid: {}".format(e)) |
| 490 | + ) |
| 491 | + if data.pop("verify"): |
| 492 | + dist = Distribution(name=data["package"].filename, digest=data["package"].sha256) |
| 493 | + try: |
| 494 | + for attestation_bundle in provenance.attestation_bundles: |
| 495 | + publisher = attestation_bundle.publisher |
| 496 | + policy = publisher._as_policy() |
| 497 | + for attestation in attestation_bundle.attestations: |
| 498 | + attestation.verify(policy, dist) |
| 499 | + except VerificationError as e: |
| 500 | + raise serializers.ValidationError(_("Provenance verification failed: {}".format(e))) |
| 501 | + return data |
| 502 | + |
| 503 | + def retrieve(self, validated_data): |
| 504 | + sha256 = python_models.PackageProvenance.calculate_sha256(validated_data["provenance"]) |
| 505 | + content = python_models.PackageProvenance.objects.filter( |
| 506 | + sha256=sha256, _pulp_domain=get_domain() |
| 507 | + ).first() |
| 508 | + return content |
| 509 | + |
| 510 | + class Meta: |
| 511 | + fields = core_serializers.NoArtifactContentUploadSerializer.Meta.fields + ( |
| 512 | + "package", |
| 513 | + "provenance", |
| 514 | + "sha256", |
| 515 | + "verify", |
| 516 | + ) |
| 517 | + model = python_models.PackageProvenance |
| 518 | + |
| 519 | + |
| 520 | +class _AttestationSerializer(serializers.Serializer): |
| 521 | + """ |
| 522 | + A simple serializer for Attestation. |
| 523 | +
|
| 524 | + Returns the information that `pypi-attestations inspect` provides. |
| 525 | + """ |
| 526 | + |
| 527 | + version = serializers.CharField(read_only=True) |
| 528 | + statement = serializers.JSONField(read_only=True) |
| 529 | + certificate = serializers.JSONField(read_only=True) |
| 530 | + transparency_log = serializers.JSONField(read_only=True) |
| 531 | + |
| 532 | + def to_representation(self, instance): |
| 533 | + statement = json.loads(instance.envelope.statement.decode()) |
| 534 | + verification_material = instance.verification_material |
| 535 | + cert = x509.load_der_x509_certificate(verification_material.certificate) |
| 536 | + san = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName) |
| 537 | + cert_info = { |
| 538 | + "Subjects": [name.value for name in san.value], |
| 539 | + "Issuer": cert.issuer.rfc4514_string(), |
| 540 | + "Validity": str(cert.not_valid_after_utc), |
| 541 | + } |
| 542 | + transparency_log = { |
| 543 | + "Log Indexes": [ |
| 544 | + entry["logIndex"] for entry in verification_material.transparency_entries |
| 545 | + ], |
| 546 | + } |
| 547 | + return { |
| 548 | + "version": instance.version, |
| 549 | + "statement": statement, |
| 550 | + "certificate": cert_info, |
| 551 | + "transparency_log": transparency_log, |
| 552 | + } |
| 553 | + |
| 554 | + |
| 555 | +class _AttestationBundleSerializer(serializers.Serializer): |
| 556 | + """ |
| 557 | + A simple serializer for AttestationBundle. |
| 558 | + """ |
| 559 | + |
| 560 | + publisher = serializers.JSONField(read_only=True) |
| 561 | + attestations = _AttestationSerializer(many=True) |
| 562 | + |
| 563 | + def to_representation(self, instance): |
| 564 | + att_field = self.fields["attestations"] |
| 565 | + return { |
| 566 | + "publisher": instance.publisher.model_dump(), |
| 567 | + "attestations": [ |
| 568 | + att_field.child.to_representation(att) for att in instance.attestations |
| 569 | + ], |
| 570 | + } |
| 571 | + |
| 572 | + |
| 573 | +class MinimalPackageProvenanceSerializer(serializers.Serializer): |
| 574 | + """ |
| 575 | + A human readable serializer for PackageProvenance. |
| 576 | + """ |
| 577 | + |
| 578 | + version = serializers.CharField(read_only=True) |
| 579 | + attestation_bundles = _AttestationBundleSerializer(many=True) |
| 580 | + |
| 581 | + def to_representation(self, instance): |
| 582 | + provenance = instance.as_model |
| 583 | + att_bund_field = self.fields["attestation_bundles"] |
| 584 | + return { |
| 585 | + "version": provenance.version, |
| 586 | + "attestation_bundles": [ |
| 587 | + att_bund_field.child.to_representation(att_bund) |
| 588 | + for att_bund in provenance.attestation_bundles |
| 589 | + ], |
| 590 | + } |
| 591 | + |
| 592 | + |
457 | 593 | class MultipleChoiceArrayField(serializers.MultipleChoiceField): |
458 | 594 | """ |
459 | 595 | A wrapper to make sure this DRF serializer works properly with ArrayFields. |
|
0 commit comments