diff --git a/app/models/packages/conan/file_metadatum.rb b/app/models/packages/conan/file_metadatum.rb
index 39dbbcf9d091d48718348d502f6d1e8cd5228a2f..fe7ec6fee1766faa12b0f2611aa7562884032fec 100644
--- a/app/models/packages/conan/file_metadatum.rb
+++ b/app/models/packages/conan/file_metadatum.rb
@@ -16,6 +16,8 @@ class Packages::Conan::FileMetadatum < ApplicationRecord
   validates :package_file, presence: true
   validates :conan_package_reference, absence: true, if: :recipe_file?
   validates :conan_package_reference, format: { with: Gitlab::Regex.conan_package_reference_regex }, if: :package_file?
+  validates :package_reference, absence: true, if: :recipe_file?
+  validates :package_reference, presence: true, if: :package_file?, on: :create
   validate :conan_package_type
   # recipe_revision and package_revision are not supported yet
   validates :recipe_revision, absence: true
diff --git a/app/models/packages/conan/package_reference.rb b/app/models/packages/conan/package_reference.rb
index 99a07300bca7f3ef7128b4840ec4abfae0747a03..f9586a8fb06a0bed99a05ba350024d1c8dd4bec5 100644
--- a/app/models/packages/conan/package_reference.rb
+++ b/app/models/packages/conan/package_reference.rb
@@ -5,7 +5,7 @@ module Conan
     class PackageReference < ApplicationRecord
       include ShaAttribute
 
-      REFERENCE_LENGTH_MAX = 20
+      REFERENCE_LENGTH_MAX = 40
       MAX_INFO_SIZE = 20_000
 
       sha_attribute :reference
@@ -20,11 +20,16 @@ class PackageReference < ApplicationRecord
       has_many :file_metadata, inverse_of: :package_reference, class_name: 'Packages::Conan::FileMetadatum'
 
       validates :package, :project, presence: true
-      validates :reference, presence: true, bytesize: { maximum: -> { REFERENCE_LENGTH_MAX } },
-        uniqueness: { scope: [:package_id, :recipe_revision_id] }
+      validates :reference, presence: true, bytesize: { maximum: -> { REFERENCE_LENGTH_MAX } }
+      validates :reference, uniqueness: { scope: %i[package_id recipe_revision_id] }, on: %i[create update]
+
       validates :info, json_schema: { filename: 'conan_package_info', detail_errors: true }
       validate :ensure_info_size
 
+      def self.for_package_id_and_reference(package_id, reference)
+        where(package_id: package_id, reference: reference)
+      end
+
       private
 
       def ensure_info_size
diff --git a/app/models/packages/conan/package_revision.rb b/app/models/packages/conan/package_revision.rb
index 45e6fc3d64a6cbe2511e095fa7573ab70f7f77ce..5e7973419948e911ff07f913a83ad42af14345d8 100644
--- a/app/models/packages/conan/package_revision.rb
+++ b/app/models/packages/conan/package_revision.rb
@@ -5,7 +5,7 @@ module Conan
     class PackageRevision < ApplicationRecord
       include ShaAttribute
 
-      REVISION_LENGTH_MAX = 20
+      REVISION_LENGTH_MAX = 40
 
       sha_attribute :revision
 
diff --git a/app/models/packages/conan/recipe_revision.rb b/app/models/packages/conan/recipe_revision.rb
index fd677aba71eff553aaeada15f83267142a3baae9..3f5d915ad74b93369e009502b088955242215ebd 100644
--- a/app/models/packages/conan/recipe_revision.rb
+++ b/app/models/packages/conan/recipe_revision.rb
@@ -5,7 +5,7 @@ module Conan
     class RecipeRevision < ApplicationRecord
       include ShaAttribute
 
-      REVISION_LENGTH_MAX = 20
+      REVISION_LENGTH_MAX = 40
 
       sha_attribute :revision
 
diff --git a/app/services/packages/conan/create_package_file_service.rb b/app/services/packages/conan/create_package_file_service.rb
index 0ca3747a323e40d672217fc196c656721c2e42da..cedcf3676f295049e48554607bc2a1ef00379473 100644
--- a/app/services/packages/conan/create_package_file_service.rb
+++ b/app/services/packages/conan/create_package_file_service.rb
@@ -3,8 +3,6 @@
 module Packages
   module Conan
     class CreatePackageFileService
-      attr_reader :package, :file, :params
-
       def initialize(package, file, params)
         @package = package
         @file = file
@@ -12,24 +10,42 @@ def initialize(package, file, params)
       end
 
       def execute
-        package_file = package.package_files.build(
-          file: file,
-          size: params['file.size'],
-          file_name: params[:file_name],
-          file_sha1: params['file.sha1'],
-          file_md5: params['file.md5'],
-          conan_file_metadatum_attributes: {
-            conan_package_reference: params[:conan_package_reference],
-            conan_file_type: params[:conan_file_type]
-          }
-        )
-
-        if params[:build].present?
-          package_file.package_file_build_infos << package_file.package_file_build_infos.build(pipeline: params[:build].pipeline)
+        package_file = nil
+        ApplicationRecord.transaction do
+          package_file = package.package_files.build(
+            file: file,
+            size: params['file.size'],
+            file_name: params[:file_name],
+            file_sha1: params['file.sha1'],
+            file_md5: params['file.md5'],
+            conan_file_metadatum_attributes: {
+              conan_package_reference: params[:conan_package_reference],
+              conan_file_type: params[:conan_file_type],
+              package_reference_id: package_reference_id
+            }
+          )
+
+          if params[:build].present?
+            package_file.package_file_build_infos << package_file.package_file_build_infos.build(pipeline: params[:build].pipeline)
+          end
+
+          package_file.save!
         end
 
-        package_file.save!
-        package_file
+        ServiceResponse.success(payload: { package_file: package_file })
+      rescue ActiveRecord::RecordInvalid => e
+        ServiceResponse.error(message: e.message, reason: :invalid_package_file)
+      end
+
+      private
+
+      attr_reader :package, :file, :params
+
+      def package_reference_id
+        return unless params[:conan_package_reference].present?
+
+        package_reference_result = ::Packages::Conan::UpsertPackageReferenceService.new(package, params[:conan_package_reference]).execute!
+        package_reference_result[:package_reference_id]
       end
     end
   end
diff --git a/app/services/packages/conan/upsert_package_reference_service.rb b/app/services/packages/conan/upsert_package_reference_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..59f61ba31daf86cefc34934ca9b930da49a7a2a0
--- /dev/null
+++ b/app/services/packages/conan/upsert_package_reference_service.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module Packages
+  module Conan
+    class UpsertPackageReferenceService
+      include Gitlab::Utils::StrongMemoize
+
+      def initialize(package, package_reference_value)
+        @package = package
+        @package_reference_value = package_reference_value
+      end
+
+      def execute!
+        # We use a different validation context
+        # so that the uniqueness model validation on
+        # [reference, package_id, recipe_revision_id]
+        # is skipped.
+        package_reference.validate!(:upsert)
+
+        ServiceResponse.success(payload: { package_reference_id: upsert_package_reference[0]['id'] })
+      end
+
+      private
+
+      attr_reader :package, :package_reference_value
+
+      def package_reference
+        ::Packages::Conan::PackageReference.new(
+          package_id: package.id,
+          reference: package_reference_value,
+          project_id: package.project_id
+        )
+      end
+      strong_memoize_attr :package_reference
+
+      def upsert_package_reference
+        ::Packages::Conan::PackageReference
+          .upsert(
+            package_reference.attributes.slice('package_id', 'project_id', 'reference'),
+            unique_by: %i[package_id reference]
+          )
+      end
+    end
+  end
+end
diff --git a/db/migrate/20241105181558_add_uniq_index_package_reference.rb b/db/migrate/20241105181558_add_uniq_index_package_reference.rb
new file mode 100644
index 0000000000000000000000000000000000000000..290338e4127d624761d59a8a04c0e59aab200e11
--- /dev/null
+++ b/db/migrate/20241105181558_add_uniq_index_package_reference.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class AddUniqIndexPackageReference < Gitlab::Database::Migration[2.2]
+  milestone '17.7'
+
+  INDEX_NAME = 'uniq_index_pkg_refs_on_ref_and_pkg_id_when_rev_is_null'
+
+  disable_ddl_transaction!
+
+  def up
+    add_concurrent_index :packages_conan_package_references,
+      [:package_id, :reference],
+      unique: true,
+      name: INDEX_NAME,
+      where: 'recipe_revision_id IS NULL'
+  end
+
+  def down
+    remove_concurrent_index_by_name :packages_conan_package_references, INDEX_NAME
+  end
+end
diff --git a/db/migrate/20241129142800_add_check_constraint_to_conan_file_metadata.rb b/db/migrate/20241129142800_add_check_constraint_to_conan_file_metadata.rb
new file mode 100644
index 0000000000000000000000000000000000000000..716fa13c742e56cb0698a9875c1f6093e3e7da7a
--- /dev/null
+++ b/db/migrate/20241129142800_add_check_constraint_to_conan_file_metadata.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddCheckConstraintToConanFileMetadata < Gitlab::Database::Migration[2.2]
+  milestone '17.7'
+
+  disable_ddl_transaction!
+
+  CONSTRAINT_NAME = 'check_conan_file_metadata_ref_null_for_recipe_files'
+  RECIPE_FILE = 1
+
+  def up
+    add_check_constraint :packages_conan_file_metadata,
+      "NOT (conan_file_type = #{RECIPE_FILE} AND package_reference_id IS NOT NULL)", CONSTRAINT_NAME
+  end
+
+  def down
+    remove_check_constraint :packages_conan_file_metadata, CONSTRAINT_NAME
+  end
+end
diff --git a/db/schema_migrations/20241105181558 b/db/schema_migrations/20241105181558
new file mode 100644
index 0000000000000000000000000000000000000000..5a1518bb451d138c97add5b591b70b1dbb91b511
--- /dev/null
+++ b/db/schema_migrations/20241105181558
@@ -0,0 +1 @@
+75ba7fa0b8cbb9ffbbc131e72ef208c3bbc4a2165508d624f1827e881f900329
\ No newline at end of file
diff --git a/db/schema_migrations/20241129142800 b/db/schema_migrations/20241129142800
new file mode 100644
index 0000000000000000000000000000000000000000..5b1961807fb71c16dd17de61923f94a81e6ffe7c
--- /dev/null
+++ b/db/schema_migrations/20241129142800
@@ -0,0 +1 @@
+5fa507e11997131856fd0b9c2b0408e86bb20719c0896fd1966d83b856a1cb81
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 07e590b0ae0d309645887fb51563abbd67f6c08b..d0d698ce3a133495673f53ad33ef245d22d8caca 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -16156,7 +16156,8 @@ CREATE TABLE packages_conan_file_metadata (
     conan_file_type smallint NOT NULL,
     recipe_revision_id bigint,
     package_revision_id bigint,
-    package_reference_id bigint
+    package_reference_id bigint,
+    CONSTRAINT check_conan_file_metadata_ref_null_for_recipe_files CHECK ((NOT ((conan_file_type = 1) AND (package_reference_id IS NOT NULL))))
 );
 
 CREATE SEQUENCE packages_conan_file_metadata_id_seq
@@ -33564,6 +33565,8 @@ CREATE UNIQUE INDEX uniq_idx_subscription_seat_assignments_on_namespace_and_user
 
 CREATE UNIQUE INDEX uniq_idx_user_add_on_assignments_on_add_on_purchase_and_user ON subscription_user_add_on_assignments USING btree (add_on_purchase_id, user_id);
 
+CREATE UNIQUE INDEX uniq_index_pkg_refs_on_ref_and_pkg_id_when_rev_is_null ON packages_conan_package_references USING btree (package_id, reference) WHERE (recipe_revision_id IS NULL);
+
 CREATE UNIQUE INDEX uniq_pkgs_deb_grp_architectures_on_distribution_id_and_name ON packages_debian_group_architectures USING btree (distribution_id, name);
 
 CREATE UNIQUE INDEX uniq_pkgs_deb_grp_components_on_distribution_id_and_name ON packages_debian_group_components USING btree (distribution_id, name);
diff --git a/lib/api/helpers/packages/conan/api_helpers.rb b/lib/api/helpers/packages/conan/api_helpers.rb
index 435b2690184a2cbdddb8a6126a96d8b1a7357a58..9aa478756f737b76ab40361599f3a495e5126ebd 100644
--- a/lib/api/helpers/packages/conan/api_helpers.rb
+++ b/lib/api/helpers/packages/conan/api_helpers.rb
@@ -230,7 +230,12 @@ def upload_package_file(file_type)
 
             track_push_package_event unless params[:file].empty_size?
 
-            create_package_file_with_type(file_type, current_package)
+            service_response = create_package_file_with_type(file_type, current_package)
+            return unless service_response
+
+            bad_request!(service_response.message) if service_response.error?
+
+            service_response[:package_file]
           rescue ObjectStorage::RemoteStoreError => e
             Gitlab::ErrorTracking.track_exception(e, file_name: params[:file_name], project_id: project.id)
 
diff --git a/spec/factories/packages/conan/file_metadata.rb b/spec/factories/packages/conan/file_metadata.rb
index 89cd05bdd5ddaa508ba954a32e94cfeffae754e7..f445337f4a04b211f17d68d67fee3e48329a0f99 100644
--- a/spec/factories/packages/conan/file_metadata.rb
+++ b/spec/factories/packages/conan/file_metadata.rb
@@ -12,7 +12,8 @@
     trait(:package_file) do
       package_file { association(:conan_package_file, :conan_package, without_loaded_metadatum: true) }
       conan_file_type { 'package_file' }
-      conan_package_reference { '123456789' }
+      package_reference { association(:conan_package_reference) }
+      conan_package_reference { package_reference.reference }
     end
   end
 end
diff --git a/spec/factories/packages/conan/package_references.rb b/spec/factories/packages/conan/package_references.rb
index 791e6818a5bff15f53750e10c0c6e8219c8e6c39..792cd43b097db614c66252b26296c35214138186 100644
--- a/spec/factories/packages/conan/package_references.rb
+++ b/spec/factories/packages/conan/package_references.rb
@@ -4,7 +4,7 @@
   factory :conan_package_reference, class: 'Packages::Conan::PackageReference' do
     package { association(:conan_package) }
     project { association(:project) }
-    recipe_revision { association(:conan_recipe_revision) }
+    recipe_revision { association(:conan_recipe_revision, package: package, project: project) }
     info do
       {
         settings: { os: 'Linux', arch: 'x86_64' },
@@ -13,6 +13,6 @@
         otherProperties: 'some_value'
       }
     end
-    sequence(:reference) { |n| Digest::SHA1.digest(n.to_s) } # rubocop:disable Fips/SHA1 -- The conan registry is not FIPS compliant
+    sequence(:reference) { |n| Digest::SHA1.hexdigest(n.to_s) } # rubocop:disable Fips/SHA1 -- The conan registry is not FIPS compliant
   end
 end
diff --git a/spec/factories/packages/conan/package_revision.rb b/spec/factories/packages/conan/package_revision.rb
index 7c8a6bcdcda31eefa66c99faa6d8c5e47388d994..2b7135589f4d3a595d8e18d0875839f10e72e9bb 100644
--- a/spec/factories/packages/conan/package_revision.rb
+++ b/spec/factories/packages/conan/package_revision.rb
@@ -5,6 +5,6 @@
     package { association(:conan_package) }
     association :project
     package_reference { association(:conan_package_reference) }
-    sequence(:revision) { |n| Digest::SHA1.digest(n.to_s) } # rubocop:disable Fips/SHA1 -- The conan registry is not FIPS compliant
+    sequence(:revision) { |n| Digest::SHA1.hexdigest(n.to_s) } # rubocop:disable Fips/SHA1 -- The conan registry is not FIPS compliant
   end
 end
diff --git a/spec/factories/packages/conan/packages.rb b/spec/factories/packages/conan/packages.rb
index e5ad7d35621aeef389e2ccb161798a99796bb760..1f9d5ed85eeadd5b9b7f3c58450818695655b5e9 100644
--- a/spec/factories/packages/conan/packages.rb
+++ b/spec/factories/packages/conan/packages.rb
@@ -10,6 +10,14 @@
 
     transient do
       without_package_files { false }
+      package_references { ['1234567890abcdef1234567890abcdef12345678'] }
+    end
+
+    conan_package_references do
+      package_references.map do |ref|
+        association(:conan_package_reference,
+          reference: ref, package: instance)
+      end
     end
 
     after :build do |package|
@@ -20,10 +28,17 @@
 
     after :create do |package, evaluator|
       unless evaluator.without_package_files
-        %i[conan_recipe_file conan_recipe_manifest conan_package_info conan_package_manifest
-          conan_package].each do |file|
+        %i[conan_recipe_file conan_recipe_manifest].each do |file|
           create :conan_package_file, file, package: package
         end
+
+        package_file_traits = %i[conan_package_info conan_package_manifest conan_package]
+        package.conan_package_references.each do |reference|
+          package_file_traits.each do |file|
+            create :conan_package_file, file, package: package,
+              conan_package_reference: reference
+          end
+        end
       end
     end
 
diff --git a/spec/factories/packages/conan/recipe_revisions.rb b/spec/factories/packages/conan/recipe_revisions.rb
index 155558f460f7557c099ff7eb54f2573a9239be5c..a82a4bd798b8171cb1ae4339d468a4a5be7ac02a 100644
--- a/spec/factories/packages/conan/recipe_revisions.rb
+++ b/spec/factories/packages/conan/recipe_revisions.rb
@@ -4,6 +4,6 @@
   factory :conan_recipe_revision, class: 'Packages::Conan::RecipeRevision' do
     package { association(:conan_package) }
     association :project
-    sequence(:revision) { |n| Digest::SHA1.digest(n.to_s) } # rubocop:disable Fips/SHA1 -- The conan registry is not FIPS compliant
+    sequence(:revision) { |n| Digest::SHA1.hexdigest(n.to_s) } # rubocop:disable Fips/SHA1 -- The conan registry is not FIPS compliant
   end
 end
diff --git a/spec/factories/packages/package_files.rb b/spec/factories/packages/package_files.rb
index aa4bf58a663d3bb5f3b0e6c596255f2588501169..822fd80f3e254827862e0ac10e07434ee64335b6 100644
--- a/spec/factories/packages/package_files.rb
+++ b/spec/factories/packages/package_files.rb
@@ -25,6 +25,7 @@
 
       transient do
         without_loaded_metadatum { false }
+        conan_package_reference { package.conan_package_references.first }
       end
 
       trait(:conan_recipe_file) do
@@ -58,7 +59,8 @@
       trait(:conan_package_manifest) do
         after :create do |package_file, evaluator|
           unless evaluator.without_loaded_metadatum
-            create :conan_file_metadatum, :package_file, package_file: package_file
+            create :conan_file_metadatum, :package_file,
+              { package_file: package_file, package_reference: evaluator.conan_package_reference }.compact
           end
         end
 
@@ -72,7 +74,8 @@
       trait(:conan_package_info) do
         after :create do |package_file, evaluator|
           unless evaluator.without_loaded_metadatum
-            create :conan_file_metadatum, :package_file, package_file: package_file
+            create :conan_file_metadatum, :package_file,
+              { package_file: package_file, package_reference: evaluator.conan_package_reference }.compact
           end
         end
 
@@ -86,7 +89,8 @@
       trait(:conan_package) do
         after :create do |package_file, evaluator|
           unless evaluator.without_loaded_metadatum
-            create :conan_file_metadatum, :package_file, package_file: package_file
+            create :conan_file_metadatum, :package_file,
+              { package_file: package_file, package_reference: evaluator.conan_package_reference }.compact
           end
         end
 
diff --git a/spec/models/packages/conan/file_metadatum_spec.rb b/spec/models/packages/conan/file_metadatum_spec.rb
index efae7cc570ad7cbc1afd0653e3f378595cfbcc41..15d6f509e7ccbd7892685e9664836e1175c6812f 100644
--- a/spec/models/packages/conan/file_metadatum_spec.rb
+++ b/spec/models/packages/conan/file_metadatum_spec.rb
@@ -3,7 +3,7 @@
 require 'spec_helper'
 
 RSpec.describe Packages::Conan::FileMetadatum, type: :model do
-  let_it_be(:package_file) { create(:conan_package_file, :conan_recipe_file) }
+  let_it_be(:package_file) { build(:conan_package_file) }
 
   describe 'relationships' do
     it { is_expected.to belong_to(:package_file) }
@@ -31,7 +31,7 @@
 
     describe '#conan_package_reference' do
       context 'recipe file' do
-        let(:conan_file_metadatum) { build(:conan_file_metadatum, :recipe_file, package_file: package_file) }
+        let_it_be(:conan_file_metadatum) { build(:conan_file_metadatum, :recipe_file, package_file: package_file) }
 
         it 'is valid with empty value' do
           conan_file_metadatum.conan_package_reference = nil
@@ -47,7 +47,7 @@
       end
 
       context 'package file' do
-        let(:conan_file_metadatum) { build(:conan_file_metadatum, :package_file, package_file: package_file) }
+        let_it_be(:conan_file_metadatum) { build(:conan_file_metadatum, :package_file, package_file: package_file) }
 
         it 'is valid with acceptable value' do
           conan_file_metadatum.conan_package_reference = '123456asdf'
@@ -69,6 +69,62 @@
       end
     end
 
+    describe '#package_reference' do
+      let_it_be(:package_reference) { build(:conan_package_reference) }
+
+      context 'recipe file' do
+        let(:conan_file_metadatum) { build(:conan_file_metadatum, :recipe_file, package_file: package_file) }
+
+        it 'is valid when package_reference is absent' do
+          conan_file_metadatum.package_reference = nil
+
+          expect(conan_file_metadatum).to be_valid
+        end
+
+        it 'is invalid when package_reference is present' do
+          conan_file_metadatum.package_reference = package_reference
+
+          expect(conan_file_metadatum).to be_invalid
+        end
+      end
+
+      context 'package file' do
+        context 'on create' do
+          let(:conan_file_metadatum) { build(:conan_file_metadatum, :package_file, package_file: package_file) }
+
+          it 'is valid when package_reference is present' do
+            conan_file_metadatum.package_reference = package_reference
+
+            expect(conan_file_metadatum).to be_valid
+          end
+
+          it 'is invalid when package_reference is absent' do
+            conan_file_metadatum.package_reference = nil
+
+            expect(conan_file_metadatum).to be_invalid
+          end
+        end
+
+        context 'on update' do
+          let_it_be_with_reload(:existing_metadatum) do
+            create(:conan_file_metadatum, :package_file, package_file: package_file)
+          end
+
+          it 'is valid when package_reference is absent' do
+            existing_metadatum.package_reference = nil
+
+            expect(existing_metadatum).to be_valid
+          end
+
+          it 'is valid when package_reference is present' do
+            existing_metadatum.package_reference = package_reference
+
+            expect(existing_metadatum).to be_valid
+          end
+        end
+      end
+    end
+
     describe '#conan_package_type' do
       it 'validates package of type conan' do
         package = build('package')
diff --git a/spec/models/packages/conan/package_reference_spec.rb b/spec/models/packages/conan/package_reference_spec.rb
index a94b30531f8305ada5e8d91b98147f8ac133e0c8..3b8bff658068162835e2880b51d4241df9826d7d 100644
--- a/spec/models/packages/conan/package_reference_spec.rb
+++ b/spec/models/packages/conan/package_reference_spec.rb
@@ -33,10 +33,48 @@
     it { is_expected.to validate_presence_of(:project) }
     it { is_expected.to validate_presence_of(:reference) }
 
-    it do
-      # ignore case, same revision string with different case are converted to same hexa binary
-      is_expected.to validate_uniqueness_of(:reference).scoped_to([:package_id,
-        :recipe_revision_id]).case_insensitive
+    describe 'uniqueness of reference' do
+      let_it_be(:conan_package) { create(:conan_package, without_package_files: true) }
+      let_it_be(:existing_reference) { create(:conan_package_reference, package: conan_package) }
+
+      context 'when recipe_revision_id is not nil' do
+        it 'validates uniqueness scoped to package_id and recipe_revision_id', :aggregate_failures do
+          duplicate_reference = build(:conan_package_reference, package_id: existing_reference.package_id,
+            recipe_revision_id: existing_reference.recipe_revision_id, reference: existing_reference.reference)
+
+          expect(duplicate_reference).not_to be_valid
+          expect(duplicate_reference.errors[:reference]).to include('has already been taken')
+        end
+
+        it 'is valid if the reference is unique' do
+          new_reference = build(:conan_package_reference, package_id: existing_reference.package_id,
+            recipe_revision_id: existing_reference.recipe_revision_id,
+            reference: Digest::SHA1.hexdigest('new_unique_reference')) # rubocop:disable Fips/SHA1 -- The conan registry is not FIPS compliant
+
+          expect(new_reference).to be_valid
+        end
+      end
+
+      context 'when recipe_revision_id is nil' do
+        let_it_be(:existing_nil_revision_reference) do
+          create(:conan_package_reference, package: conan_package, recipe_revision_id: nil)
+        end
+
+        it 'validates uniqueness scoped to package_id when both have nil recipe_revision_id', :aggregate_failures do
+          duplicate_reference = build(:conan_package_reference, package_id: existing_nil_revision_reference.package_id,
+            recipe_revision_id: nil, reference: existing_nil_revision_reference.reference)
+
+          expect(duplicate_reference).not_to be_valid
+          expect(duplicate_reference.errors[:reference]).to include('has already been taken')
+        end
+
+        it 'is valid if existing reference has a non-nil recipe_revision_id' do
+          duplicate_reference = build(:conan_package_reference, package_id: existing_reference.package_id,
+            recipe_revision_id: nil, reference: existing_reference.reference)
+
+          expect(duplicate_reference).to be_valid
+        end
+      end
     end
 
     context 'on reference' do
@@ -109,4 +147,20 @@
       end
     end
   end
+
+  describe '.for_package_id_and_reference' do
+    let_it_be(:package_reference) { create(:conan_package_reference) }
+
+    let(:reference) { package_reference.reference }
+
+    subject { described_class.for_package_id_and_reference(package_reference.package_id, reference) }
+
+    it { is_expected.to contain_exactly(package_reference) }
+
+    context 'when no match is found' do
+      let(:reference) { 'non_existent_reference' }
+
+      it { is_expected.to be_empty }
+    end
+  end
 end
diff --git a/spec/presenters/packages/conan/package_presenter_spec.rb b/spec/presenters/packages/conan/package_presenter_spec.rb
index 0fe810ef59caf1c772d27242e8b55ab667ae3fe1..71e10e0c2540a59a4f849dd59a5b8c380f5cc990 100644
--- a/spec/presenters/packages/conan/package_presenter_spec.rb
+++ b/spec/presenters/packages/conan/package_presenter_spec.rb
@@ -4,10 +4,11 @@
 
 RSpec.describe ::Packages::Conan::PackagePresenter, feature_category: :package_registry do
   let_it_be(:user) { create(:user) }
-  let_it_be(:package) { create(:conan_package) }
+  let_it_be(:conan_package_reference) { '1234567890abcdef1234567890abcdef12345678' }
+  let_it_be(:alternative_reference) { '1111111111111111111111111111111111111111' }
+  let_it_be(:package) { create(:conan_package, package_references: [conan_package_reference, alternative_reference]) }
   let_it_be(:project) { package.project }
   let_it_be(:package_file_pending_destruction) { create(:package_file, :pending_destruction, package: package) }
-  let_it_be(:conan_package_reference) { '123456789' }
 
   let(:params) { { package_scope: :instance } }
   let(:presenter) { described_class.new(package, user, project, params) }
@@ -143,18 +144,6 @@
       end
 
       context 'multiple packages with different references' do
-        let(:info_file) { create(:conan_package_file, :conan_package_info, package: package) }
-        let(:manifest_file) { create(:conan_package_file, :conan_package_manifest, package: package) }
-        let(:package_file) { create(:conan_package_file, :conan_package, package: package) }
-        let(:alternative_reference) { 'abcdefghi' }
-
-        before do
-          [info_file, manifest_file, package_file].each do |file|
-            file.conan_file_metadatum.conan_package_reference = alternative_reference
-            file.save!
-          end
-        end
-
         it { is_expected.to eq(expected_result) }
 
         context 'requesting the alternative reference' do
diff --git a/spec/services/packages/conan/create_package_file_service_spec.rb b/spec/services/packages/conan/create_package_file_service_spec.rb
index 3668ebf9ffb92c715a8290adddbc262c5e5f721c..0edaf2acc2796ed8b5a5d654840b7c0a79914699 100644
--- a/spec/services/packages/conan/create_package_file_service_spec.rb
+++ b/spec/services/packages/conan/create_package_file_service_spec.rb
@@ -4,13 +4,14 @@
 RSpec.describe Packages::Conan::CreatePackageFileService, feature_category: :package_registry do
   include WorkhorseHelpers
 
-  let_it_be(:package) { create(:conan_package) }
+  let_it_be(:package) { create(:conan_package, without_package_files: true) }
   let_it_be(:user) { create(:user) }
 
-  describe '#execute' do
+  describe '#execute', :aggregate_failures do
     let(:file_name) { 'foo.tgz' }
+    let(:conan_package_reference) { '1234567890abcdef1234567890abcdef12345678' }
 
-    subject { described_class.new(package, file, params).execute }
+    subject(:response) { described_class.new(package, file, params).execute }
 
     shared_examples 'a valid package_file' do
       let(:params) do
@@ -22,13 +23,16 @@
           'file.type': 'txt',
           recipe_revision: '0',
           package_revision: '0',
-          conan_package_reference: '123456789',
+          conan_package_reference: conan_package_reference,
           conan_file_type: :package_file
         }.with_indifferent_access
       end
 
       it 'creates a new package file' do
-        package_file = subject
+        response = subject
+
+        expect(response).to be_success
+        package_file = response[:package_file]
 
         expect(package_file).to be_valid
         expect(package_file.file_name).to eq(file_name)
@@ -39,12 +43,15 @@
         expect(package_file.conan_file_metadatum.package_revision_value).to eq('0')
         expect(package_file.conan_file_metadatum.recipe_revision_id).to be_nil
         expect(package_file.conan_file_metadatum.package_revision_id).to be_nil
-        expect(package_file.conan_file_metadatum.conan_package_reference).to eq('123456789')
+        expect(package_file.conan_file_metadatum.conan_package_reference).to eq(conan_package_reference)
+        expect(package_file.conan_file_metadatum.package_reference.reference).to eq(conan_package_reference)
         expect(package_file.conan_file_metadatum.conan_file_type).to eq('package_file')
         expect(package_file.file.read).to eq('content')
       end
 
-      it_behaves_like 'assigns build to package file'
+      it_behaves_like 'assigns build to package file' do
+        subject(:package_file) { response[:package_file] }
+      end
     end
 
     shared_examples 'a valid recipe_file' do
@@ -61,7 +68,10 @@
       end
 
       it 'creates a new recipe file' do
-        package_file = subject
+        response = subject
+
+        expect(response).to be_success
+        package_file = response[:package_file]
 
         expect(package_file).to be_valid
         expect(package_file.file_name).to eq(file_name)
@@ -73,14 +83,17 @@
         expect(package_file.conan_file_metadatum.recipe_revision_id).to be_nil
         expect(package_file.conan_file_metadatum.package_revision_id).to be_nil
         expect(package_file.conan_file_metadatum.conan_package_reference).to be_nil
+        expect(package_file.conan_file_metadatum.package_reference).to be_nil
         expect(package_file.conan_file_metadatum.conan_file_type).to eq('recipe_file')
         expect(package_file.file.read).to eq('content')
       end
 
-      it_behaves_like 'assigns build to package file'
+      it_behaves_like 'assigns build to package file' do
+        subject(:package_file) { response[:package_file] }
+      end
     end
 
-    context 'with temp file' do
+    shared_context 'with temp file setup' do
       let!(:file) do
         upload_path = ::Packages::PackageFileUploader.workhorse_local_upload_path
         file_path = upload_path + '/' + file_name
@@ -94,6 +107,10 @@
       before do
         allow_any_instance_of(Packages::PackageFileUploader).to receive(:size).and_return(128)
       end
+    end
+
+    context 'with temp file' do
+      include_context 'with temp file setup'
 
       it_behaves_like 'a valid package_file'
       it_behaves_like 'a valid recipe_file'
@@ -123,16 +140,77 @@
 
     context 'file is missing' do
       let(:file) { nil }
+
+      context 'with recipe_file' do
+        let(:params) do
+          {
+            file_name: file_name,
+            recipe_revision: '0',
+            conan_file_type: :recipe_file
+          }
+        end
+
+        it 'returns an error response' do
+          response = subject
+
+          expect(response).to be_error
+          expect(response.message).to eq('Validation failed: File can\'t be blank')
+          expect(response.reason).to eq(:invalid_package_file)
+          expect { response }.not_to change { Packages::PackageFile.count }
+          expect { response }.not_to change { Packages::Conan::FileMetadatum.count }
+        end
+      end
+
+      context 'with package_file' do
+        let(:params) do
+          {
+            file_name: file_name,
+            package_revision: '0',
+            conan_file_type: :package_file,
+            conan_package_reference: conan_package_reference
+          }
+        end
+
+        it 'returns an error response' do
+          response = subject
+
+          expect(response).to be_error
+          expect(response.message).to eq('Validation failed: File can\'t be blank')
+          expect(response.reason).to eq(:invalid_package_file)
+          expect { response }.not_to change { Packages::PackageFile.count }
+          expect { response }.not_to change { Packages::Conan::FileMetadatum.count }
+          expect { response }.not_to change { Packages::Conan::PackageReference.count }
+        end
+      end
+    end
+
+    context 'when an invalid conan package reference is provided' do
+      include_context 'with temp file setup'
+
       let(:params) do
         {
           file_name: file_name,
+          'file.md5': '12345',
+          'file.sha1': '54321',
+          'file.size': '128',
+          'file.type': 'txt',
           recipe_revision: '0',
-          conan_file_type: :recipe_file
-        }
+          package_revision: '0',
+          conan_package_reference: 'invalid_reference',
+          conan_file_type: :package_file
+        }.with_indifferent_access
       end
 
-      it 'raises an error' do
-        expect { subject }.to raise_error(ActiveRecord::RecordInvalid)
+      it 'returns an error response and does not create a package file' do
+        response = subject
+
+        expect(response).to be_error
+        expect(response.message).to include('Validation failed: ' \
+          'Conan file metadatum conan package reference is invalid')
+        expect(response.reason).to eq(:invalid_package_file)
+        expect { response }.not_to change { Packages::PackageFile.count }
+        expect { response }.not_to change { Packages::Conan::FileMetadatum.count }
+        expect { response }.not_to change { Packages::Conan::PackageReference.count }
       end
     end
   end
diff --git a/spec/services/packages/conan/upsert_package_reference_service_spec.rb b/spec/services/packages/conan/upsert_package_reference_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..68e72868b2c4ea4c25782299e36c9674c60fd702
--- /dev/null
+++ b/spec/services/packages/conan/upsert_package_reference_service_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Packages::Conan::UpsertPackageReferenceService, feature_category: :package_registry do
+  let_it_be(:package) { create(:conan_package, without_package_files: true, package_references: []) }
+  let_it_be(:conan_package_reference) { '1234567890abcdef1234567890abcdef12345678' }
+
+  describe '#execute!', :aggregate_failures do
+    subject(:response) { described_class.new(package, conan_package_reference).execute! }
+
+    context 'when the package reference doesn\'t exist' do
+      it 'creates the package reference' do
+        expect { response }.to change { Packages::Conan::PackageReference.count }.by(1)
+
+        package_reference = Packages::Conan::PackageReference.last
+        expect(package_reference.reference).to eq(conan_package_reference)
+        expect(response).to be_success
+        expect(response[:package_reference_id]).to eq(package_reference.id)
+      end
+
+      context 'when the package reference is invalid' do
+        let(:conan_package_reference) { nil }
+
+        it 'raises the error' do
+          expect { response }.to raise_error(ActiveRecord::RecordInvalid)
+        end
+      end
+    end
+
+    context 'when the package reference already exists' do
+      let_it_be(:package_reference) do
+        create(:conan_package_reference, package: package, reference: conan_package_reference, recipe_revision: nil)
+      end
+
+      it 'returns existing package reference' do
+        expect { response }.not_to change { Packages::Conan::PackageReference.count }
+
+        expect(response).to be_success
+        expect(response[:package_reference_id]).to eq(package_reference.id)
+      end
+    end
+  end
+end
diff --git a/spec/support/shared_contexts/requests/api/conan_packages_shared_context.rb b/spec/support/shared_contexts/requests/api/conan_packages_shared_context.rb
index c2e6980f81bc869f39db294587043d63abadc744..4cc1588726d2c4175175e096f1901fb59fa6fd05 100644
--- a/spec/support/shared_contexts/requests/api/conan_packages_shared_context.rb
+++ b/spec/support/shared_contexts/requests/api/conan_packages_shared_context.rb
@@ -18,7 +18,10 @@
 
   let_it_be(:job, freeze: true) { create(:ci_build, :running, user: user, project: project) }
 
-  let_it_be_with_reload(:package) { create(:conan_package, project: project) }
+  let_it_be(:conan_package_reference) { '1234567890abcdef1234567890abcdef12345678' }
+  let_it_be_with_reload(:package) do
+    create(:conan_package, project: project, package_references: [conan_package_reference])
+  end
 
   let(:job_token) { job.token }
   let(:auth_token) { personal_access_token.token }
@@ -50,7 +53,6 @@
 
   let(:jwt) { build_jwt(personal_access_token) }
   let(:headers) { build_token_auth_header(jwt.encoded) }
-  let(:conan_package_reference) { '123456789' }
 end
 
 RSpec.shared_context 'conan file download endpoints' do
diff --git a/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb
index 087830eb06bf1944d6deb69d8f8ac92a59d2943b..975f37c536d02f417ddab88181baf1631eeda9c6 100644
--- a/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb
@@ -370,9 +370,9 @@
 
   it 'returns the download_urls for the package files' do
     expected_response = {
-      'conaninfo.txt' => "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conaninfo.txt",
-      'conanmanifest.txt' => "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conanmanifest.txt",
-      'conan_package.tgz' => "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conan_package.tgz"
+      'conaninfo.txt' => "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/#{conan_package_reference}/0/conaninfo.txt",
+      'conanmanifest.txt' => "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/#{conan_package_reference}/0/conanmanifest.txt",
+      'conan_package.tgz' => "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/#{conan_package_reference}/0/conan_package.tgz"
     }
 
     subject