]> git.ipfire.org Git - pbs.git/commitdiff
keys: Refactor with new Ed25519 keys
authorMichael Tremer <michael.tremer@ipfire.org>
Thu, 1 Jun 2023 20:40:04 +0000 (20:40 +0000)
committerMichael Tremer <michael.tremer@ipfire.org>
Thu, 1 Jun 2023 20:40:33 +0000 (20:40 +0000)
Signed-off-by: Michael Tremer <michael.tremer@ipfire.org>
src/buildservice/keys.py
src/buildservice/repository.py
src/crontab/pakfire-build-service
src/database.sql
src/scripts/pakfire-build-service

index 1a37f1c1ac536b09ae8e7b14d6d8c8cdad021a26..cde9145e7009a3858ff2ec1cf04ad80ea189f54e 100644 (file)
@@ -4,6 +4,7 @@ import asyncio
 import datetime
 import io
 import logging
+import pakfire
 
 from . import base
 
@@ -12,20 +13,14 @@ from .decorators import *
 # Setup logging
 log = logging.getLogger("pbs.keys")
 
-DEFAULT_ALGORITHM = "Ed25519"
+DEFAULT_ALGORITHM = pakfire.PAKFIRE_KEY_ALGO_ED25519
 
 class Keys(base.Object):
-       def _get_keys(self, query, *args):
-               res = self.db.query(query, *args)
+       def _get_keys(self, query, *args, **kwargs):
+               return self.db.fetch_many(Key, query, *args, **kwargs)
 
-               for row in res:
-                       yield Key(self.backend, row.id, data=row)
-
-       def _get_key(self, query, *args):
-               res = self.db.get(query, *args)
-
-               if res:
-                       return Key(self.backend, res.id, data=res)
+       def _get_key(self, query, *args, **kwargs):
+               return self.db.fetch_one(Key, query, *args, **kwargs)
 
        def __iter__(self):
                return self._get_keys("""
@@ -34,87 +29,56 @@ class Keys(base.Object):
                        FROM
                                keys
                        WHERE
-                               deleted IS FALSE
+                               deleted_at IS NULL
                        ORDER BY
-                               created_at,
-                               fingerprint
+                               created_at
                """)
 
-       async def generate(self, *args, **kwargs):
+       async def create(self, *args, **kwargs):
                """
-                       Asynchronously generates a new key
+                       Creates a new key
                """
-               return await asyncio.to_thread(self._generate, *args, **kwargs)
-
-       def _generate(self, uid, email=None, algorithm=None):
-               log.debug("Generating key for %s (%s)" % (uid, algorithm))
-
-               # Append email if we have any
-               if email:
-                       uid = "%s <%s>" % (uid, email)
+               return await asyncio.to_thread(self._create, *args, **kwargs)
 
+       def _create(self, user):
                # Launch a new Pakfire instance
                with self.backend.pakfire() as p:
                        # Generate the new key
-                       key = p.generate_key(uid, algorithm=algorithm or DEFAULT_ALGORITHM)
+                       key = p.generate_key(DEFAULT_ALGORITHM)
 
-                       log.debug("Generated key %s" % key)
+                       # Export the secret key
+                       secret_key = key.export(True)
 
-                       # Store the key in the database
-                       return self._import(key)
+                       # Export the public key
+                       public_key = key.export()
 
-       def _import(self, key, parent_key=None):
-               """
-                       Imports a Pakfire key object into the database
-               """
-               return self._get_key("""
-                       INSERT INTO
-                               keys (
-                                       fingerprint,
-                                       uid,
-                                       name,
-                                       email,
-                                       created_at,
-                                       expires_at,
-                                       algo,
-                                       length,
+                       # Store the key in the database
+                       return self._get_key("""
+                               INSERT INTO
+                                       keys
+                               (
+                                       created_by,
                                        public_key,
                                        secret_key,
-                                       parent_key_id
+                                       key_id
                                )
-                       VALUES (
-                               %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s
+                               VALUES
+                               (
+                                       %s, %s, %s, %s
+                               )
+                               RETURNING *
+                               """, user, public_key, secret_key, key.id,
                        )
-                       RETURNING
-                               *
-                       """,
-                       key.fingerprint,
-                       key.uid,
-                       key.name,
-                       key.email,
-                       key.created_at,
-                       key.expires_at,
-                       key.algo,
-                       key.length,
-                       key.public_key,
-                       key.secret_key,
-                       parent_key,
-               )
 
        def get_by_id(self, id):
-               return self._get_key("SELECT * FROM keys WHERE id = %s", id)
-
-       def get_by_fingerprint(self, fingerprint):
                return self._get_key("""
                        SELECT
                                *
                        FROM
                                keys
                        WHERE
-                               deleted IS FALSE
-                       AND
-                               fingerprint = %s
-                       """, fingerprint,
+                               id = %s
+                       """, id,
                )
 
 
@@ -122,15 +86,13 @@ class Key(base.DataObject):
        table = "keys"
 
        def __repr__(self):
-               return "<%s %s %s>" % (self.__class__.__name__, self.fingerprint, self.uid)
+               return "<%s %s>" % (self.__class__.__name__, self.key_id)
 
-       def delete(self):
+       def delete(self, user=None):
                # Mark as deleted
-               self._set_attribute("deleted", True)
-
-               # Mark all subkeys as deleted
-               for subkey in self.subkeys:
-                       subkey.delete()
+               self._set_attribute_now("deleted_at")
+               if user:
+                       self._set_attribute("deleted_by", user)
 
        def has_perm(self, user):
                # Anonymous users have no permission
@@ -140,115 +102,32 @@ class Key(base.DataObject):
                # Admins have all permissions
                return user.is_admin()
 
-       @property
-       def fingerprint(self):
-               return self.data.fingerprint
-
-       @property
-       def uid(self):
-               return self.data.uid
+       # Key ID
 
        @property
-       def name(self):
-               return self.data.name
+       def key_id(self):
+               return self.key_id
 
-       @property
-       def email(self):
-               return self.data.email
+       # Created At
 
        @property
        def created_at(self):
                return self.data.created_at
 
-       # Expiration
-
-       @property
-       def expires_at(self):
-               return self.data.expires_at
-
-       def has_expired(self):
-               """
-                       Returns True if this key has expired
-               """
-               # Some keys don't expire
-               if self.expires_at is None:
-                       return False
-
-               return datetime.datetime.now() >= self.data.expires_at
-
-       @property
-       def algo(self):
-               return self.data.algo
-
-       @property
-       def length(self):
-               return self.data.length
+       # Public Key
 
        @property
        def public_key(self):
                return self.data.public_key
 
-       # Subkeys
-
-       @lazy_property
-       def subkeys(self):
-               """
-                       All subkeys
-               """
-               subkeys = self.backend.keys._get_keys("""
-                       SELECT
-                               *
-                       FROM
-                               keys
-                       WHERE
-                               deleted IS FALSE
-                       AND
-                               parent_key_id = %s
-                       ORDER BY
-                               created_at,
-                               fingerprint
-                       """, self.id,
-               )
-
-               return list(subkeys)
-
-       async def generate_subkey(self, name, algorithm=None):
-               """
-                       Generates a subkey
-               """
-               # Append name to the parent key name
-               name = "%s - %s" % (self.name, name)
-
-               # XXX This is currently creating a totally new key
-               key = await self.backend.keys.generate(name, self.email)
-
-               # XXX mark this as a subkey
-               key._set_attribute("parent_key_id", self.id)
-
-               # Append to existing list of subkeys
-               self.subkeys.append(key)
-
-               return key
-
-       # Revocation
+       # Secret Key
 
        @property
-       def revoked_at(self):
-               return self.data.revoked_at
-
-       def is_revoked(self):
-               """
-                       Returns True if this key has been revoked
-               """
-               if self.revoked_at:
-                       return True
-
-               return False
+       def secret_key(self):
+               return self.data.secret_key
 
-       async def revoke(self):
+       def _make_key(self, pakfire):
                """
-                       Revokes this key
+                       Parses the key and returns a Key object
                """
-               self._set_attribute_now("revoked_at")
-
-               pass # XXX TODO
+               return pakfire.import_key(self.secret_key)
index 351188d895a6bc1ea3381a792d12c36acd5395ac..f9ae94b9c30e2c61a3d76bbfb1e40870303dfa79 100644 (file)
@@ -19,12 +19,6 @@ from .decorators import *
 # Setup logging
 log = logging.getLogger("pbs.repositories")
 
-# How long should a key be in active use for?
-KEY_LIFETIME      = datetime.timedelta(days=365)
-
-# How long should rotating keys overlap?
-KEY_ROLLOVER_TIME = datetime.timedelta(days=60)
-
 class Repositories(base.Object):
        def _get_repositories(self, query, *args, **kwargs):
                return self.db.fetch_many(Repository, query, *args, **kwargs)
@@ -33,8 +27,17 @@ class Repositories(base.Object):
                return self.db.fetch_one(Repository, query, *args, **kwargs)
 
        def __iter__(self):
-               repositories = self._get_repositories("SELECT * FROM repositories \
-                       WHERE deleted_at IS NULL ORDER BY distro_id, name")
+               repositories = self._get_repositories("""
+                       SELECT
+                               *
+                       FROM
+                               repositories
+                       WHERE
+                               deleted_at IS NULL
+                       ORDER BY
+                               distro_id, name
+                       """,
+               )
 
                return iter(repositories)
 
@@ -65,7 +68,7 @@ class Repositories(base.Object):
                slug = self._make_slug(name, owner=owner)
 
                # Create a key for this repository
-               key = await self._make_key(distro, name, owner=owner)
+               key = await self.backend.keys.create(owner)
 
                repo = self._get_repository("""
                        INSERT INTO
@@ -86,16 +89,10 @@ class Repositories(base.Object):
                        name,
                        slug,
                        key,
-               )
 
-               # Populate cache
-               if owner:
-                       repo.owner = owner
-               if key:
-                       repo.key = key
-
-               # Rotate keys for the first time
-               await repo.rotate_keys()
+                       # Populate the cache
+                       key=key, owner=owner,
+               )
 
                return repo
 
@@ -108,23 +105,6 @@ class Repositories(base.Object):
                        if not exists:
                                return slug
 
-       async def _make_key(self, distro, name, owner=None):
-               """
-                       Generates a new key for the repository
-               """
-               # Generate the UID
-               uid = "%s - %s" % (distro, name)
-
-               # For personal repositories, we will prepend the owner
-               if owner:
-                       uid = "%s - %s" % (owner, uid)
-
-               # Fetch an email address
-               email = owner.email if owner else distro.contact
-
-               # Generate the key
-               return await self.backend.keys.generate(uid, email=email)
-
        def get_by_id(self, repo_id):
                return self._get_repository("SELECT * FROM repositories \
                        WHERE id = %s", repo_id)
@@ -136,13 +116,6 @@ class Repositories(base.Object):
                for repo in self:
                        await repo.write()
 
-       async def rotate_keys(self):
-               """
-                       Rotates keys for all repositories
-               """
-               for repo in self:
-                       await repo.rotate_keys()
-
 
 class Repository(base.DataObject):
        table = "repositories"
@@ -301,7 +274,7 @@ class Repository(base.DataObject):
                        "baseurl" : "file://%s" % self.local_path(arch) if local else self.download_url,
 
                        # Key
-                       "key" : self.key.public_key or "",
+                       "key" : self.key.public_key if self.key else "",
 
                        # Priority
                        "priority" : "%s" % self.priority,
@@ -336,66 +309,7 @@ class Repository(base.DataObject):
        def key(self):
                return self.backend.keys.get_by_id(self.data.key_id)
 
-       async def _create_subkey(self):
-               """
-                       Creates a new subkey for this repository
-               """
-               today = datetime.date.today()
-
-               # Mark the key with the data when it has been created
-               name = today.strftime("%Y-%m-%d")
-
-               # XXX add expiration
-
-               # Generate the key
-               await self.key.generate_subkey(name)
-
-       @property
-       def signing_keys(self):
-               """
-                       Returns a list of all keys that are being used to sign this repository
-               """
-               return [key for key in self.key.subkeys \
-                       if not key.has_expired() and not key.is_revoked()]
-
-       # Key Rotation
-
-       async def rotate_keys(self):
-               """
-                       This function rotates any keys (if possible)
-               """
-               log.info("Rotating keys in repository %s" % self)
-
-               # If no keys exist, create a new key
-               if not self.signing_keys:
-                       await self._create_subkey()
-                       return
-
-               # Check the time
-               now = datetime.datetime.now()
-
-               log.debug("Current signing keys:")
-               for key in self.signing_keys:
-                       log.debug("  %s" % key)
-                       log.debug("    Created at: %s" % key.created_at)
-                       if key.expires_at:
-                               log.debug("    Expires at: %s" % key.expires_at)
-
-               # Pick the key created last
-               signing_key = max(self.signing_keys, key=lambda k: k.created_at)
-
-               # Is the key expiring soon?
-               if key.expires_at and now >= key.expires_at - KEY_ROLLOVER_TIME:
-                       await self._create_subkey()
-
-               # Is this key approaching the end of its lifetime?
-               elif now >= key.created_at + KEY_LIFETIME - KEY_ROLLOVER_TIME:
-                       await self._create_subkey()
-
-               # Check if any other keys need to be revoked
-               for key in self.signing_keys:
-                       if now >= key.created_at + KEY_LIFETIME + KEY_ROLLOVER_TIME:
-                               await key.revoke()
+       # Architectures
 
        @property
        def arches(self):
@@ -1012,6 +926,7 @@ class Repository(base.DataObject):
 
        def _write(self):
                log.info("Writing repository %s..." % self)
+               key = None
 
                # Write any new repositories to the temporary space first
                tmp = self.backend.path("tmp")
@@ -1019,6 +934,10 @@ class Repository(base.DataObject):
                # Create a new pakfire instance
                with tempfile.TemporaryDirectory(dir=tmp, prefix="pakfire-repo-") as t:
                        with self.pakfire() as p:
+                               # Fetch the key
+                               if self.key:
+                                       key = self.key._make_key(p)
+
                                for arch in self.arches:
                                        # Destination path for this architecture
                                        path = os.path.join(t, arch)
@@ -1030,7 +949,7 @@ class Repository(base.DataObject):
                                        files = [p.path for p in packages if os.path.exists(p.path)]
 
                                        # Write repository metadata
-                                       p.repo_compose(path=path, files=files)
+                                       p.repo_compose(path=path, key=key, files=files)
 
                        # Mark the repository as read-only
                        os.chmod(t, 0o755)
@@ -1065,6 +984,10 @@ class Repository(base.DataObject):
                if user:
                        self._set_attribute("deleted_by", user)
 
+               # Delete the key
+               if self.key:
+                       self.key.delete()
+
                # Local path
                path = self.local_path()
 
index e08f397f739abb70de2aa7ca574530ef810d308d..7ad1516daaae7006ca85ef36c8eae11dd9420189 100644 (file)
@@ -9,9 +9,6 @@ MAILTO=pakfire@ipfire.org
 # Cleanup
 */5 * * * *            _pakfire        pakfire-build-service --logging=warning cleanup
 
-# Repositories - Key Rotation
-@daily                 _pakfire        pakfire-build-service --logging=warning repos:rotate-keys
-
 # Pull sources
 #*/5 * * * *   _pakfire        pakfire-build-service pull-sources &>/dev/null
 
index 431d4159d15428f68cb6a9efc9b48853657e05e1..c3f328203840fd939bb0142dfd76d65c11db0508 100644 (file)
@@ -97,6 +97,35 @@ CREATE TABLE public.build_watchers (
 );
 
 
+--
+-- Name: builders; Type: TABLE; Schema: public; Owner: -
+--
+
+CREATE TABLE public.builders (
+    id integer NOT NULL,
+    name text NOT NULL,
+    description text,
+    enabled boolean DEFAULT false NOT NULL,
+    loadavg text DEFAULT '0'::character varying NOT NULL,
+    maintenance boolean DEFAULT false NOT NULL,
+    max_jobs bigint DEFAULT (1)::bigint NOT NULL,
+    pakfire_version text,
+    os_name text,
+    cpu_model text,
+    cpu_count integer DEFAULT 1 NOT NULL,
+    host_key_id text,
+    updated_at timestamp without time zone,
+    cpu_arch text,
+    instance_id text,
+    instance_type text,
+    mem_total bigint,
+    created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
+    created_by integer,
+    deleted_at timestamp without time zone,
+    deleted_by integer
+);
+
+
 --
 -- Name: builds; Type: TABLE; Schema: public; Owner: -
 --
@@ -327,35 +356,6 @@ CREATE TABLE public.builder_stats (
 );
 
 
---
--- Name: builders; Type: TABLE; Schema: public; Owner: -
---
-
-CREATE TABLE public.builders (
-    id integer NOT NULL,
-    name text NOT NULL,
-    description text,
-    enabled boolean DEFAULT false NOT NULL,
-    loadavg text DEFAULT '0'::character varying NOT NULL,
-    maintenance boolean DEFAULT false NOT NULL,
-    max_jobs bigint DEFAULT (1)::bigint NOT NULL,
-    pakfire_version text,
-    os_name text,
-    cpu_model text,
-    cpu_count integer DEFAULT 1 NOT NULL,
-    host_key_id text,
-    updated_at timestamp without time zone,
-    cpu_arch text,
-    instance_id text,
-    instance_type text,
-    mem_total bigint,
-    created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
-    created_by integer,
-    deleted_at timestamp without time zone,
-    deleted_by integer
-);
-
-
 --
 -- Name: builders_id_seq; Type: SEQUENCE; Schema: public; Owner: -
 --
@@ -519,19 +519,13 @@ ALTER SEQUENCE public.jobs_id_seq OWNED BY public.jobs.id;
 
 CREATE TABLE public.keys (
     id integer NOT NULL,
-    fingerprint text NOT NULL,
-    uid text NOT NULL,
     created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
-    expires_at timestamp without time zone,
+    created_by integer,
+    deleted_at timestamp without time zone,
+    deleted_by integer,
     public_key text NOT NULL,
     secret_key text NOT NULL,
-    name text NOT NULL,
-    email text NOT NULL,
-    deleted boolean DEFAULT false NOT NULL,
-    algo text NOT NULL,
-    length integer NOT NULL,
-    parent_key_id integer,
-    revoked_at timestamp without time zone
+    key_id numeric NOT NULL
 );
 
 
@@ -540,6 +534,7 @@ CREATE TABLE public.keys (
 --
 
 CREATE SEQUENCE public.keys_id_seq
+    AS integer
     START WITH 1
     INCREMENT BY 1
     NO MINVALUE
@@ -1717,13 +1712,6 @@ CREATE INDEX jobs_running ON public.jobs USING btree (started_at) WHERE ((starte
 CREATE UNIQUE INDEX jobs_uuid ON public.jobs USING btree (uuid) WHERE (deleted_at IS NULL);
 
 
---
--- Name: keys_fingerprint; Type: INDEX; Schema: public; Owner: -
---
-
-CREATE UNIQUE INDEX keys_fingerprint ON public.keys USING btree (fingerprint) WHERE (deleted IS FALSE);
-
-
 --
 -- Name: messages_queued; Type: INDEX; Schema: public; Owner: -
 --
@@ -1773,6 +1761,13 @@ CREATE UNIQUE INDEX package_search_index_unique ON public.package_search_index U
 CREATE INDEX packages_name ON public.packages USING btree (name);
 
 
+--
+-- Name: release_monitoring_releases_build_id; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE INDEX release_monitoring_releases_build_id ON public.release_monitoring_releases USING btree (build_id);
+
+
 --
 -- Name: release_monitoring_releases_search; Type: INDEX; Schema: public; Owner: -
 --
@@ -2150,11 +2145,19 @@ ALTER TABLE ONLY public.jobs
 
 
 --
--- Name: keys keys_parent_key_id; Type: FK CONSTRAINT; Schema: public; Owner: -
+-- Name: keys keys_created_by; Type: FK CONSTRAINT; Schema: public; Owner: -
 --
 
 ALTER TABLE ONLY public.keys
-    ADD CONSTRAINT keys_parent_key_id FOREIGN KEY (parent_key_id) REFERENCES public.keys(id);
+    ADD CONSTRAINT keys_created_by FOREIGN KEY (created_by) REFERENCES public.users(id);
+
+
+--
+-- Name: keys keys_deleted_by; Type: FK CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.keys
+    ADD CONSTRAINT keys_deleted_by FOREIGN KEY (deleted_by) REFERENCES public.users(id);
 
 
 --
@@ -2213,6 +2216,14 @@ ALTER TABLE ONLY public.packages
     ADD CONSTRAINT packages_distro_id FOREIGN KEY (distro_id) REFERENCES public.distributions(id);
 
 
+--
+-- Name: release_monitoring_releases release_monitoring_releases_build_id; Type: FK CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.release_monitoring_releases
+    ADD CONSTRAINT release_monitoring_releases_build_id FOREIGN KEY (build_id) REFERENCES public.builds(id);
+
+
 --
 -- Name: release_monitoring_releases release_monitoring_releases_monitoring_id; Type: FK CONSTRAINT; Schema: public; Owner: -
 --
index 06aa0b773cae0c0292e4dbab4bd9d4b9a86923c7..8c0fa10940cff6cc0d7caa2125996e8d192c97ff 100644 (file)
@@ -31,9 +31,6 @@ class Cli(object):
                        # Jobs
                        "jobs:installcheck"   : self._jobs_installcheck,
 
-                       # Keys
-                       "keys:generate"       : self.backend.keys.generate,
-
                        # Messages
                        "messages:queue:send" : self.backend.messages.queue.send,
 
@@ -45,7 +42,6 @@ class Cli(object):
 
                        # Repositories
                        "repos:relaunch-pending-jobs" : self._repos_relaunch_pending_jobs,
-                       "repos:rotate-keys"   : self.backend.repos.rotate_keys,
                        "repos:write"         : self.backend.repos.write,
 
                        # Sources