From: Michael Tremer Date: Thu, 1 Jun 2023 20:40:04 +0000 (+0000) Subject: keys: Refactor with new Ed25519 keys X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=d779a125794904142c695296f6fd8e4b43db6e71;p=pbs.git keys: Refactor with new Ed25519 keys Signed-off-by: Michael Tremer --- diff --git a/src/buildservice/keys.py b/src/buildservice/keys.py index 1a37f1c1..cde9145e 100644 --- a/src/buildservice/keys.py +++ b/src/buildservice/keys.py @@ -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) diff --git a/src/buildservice/repository.py b/src/buildservice/repository.py index 351188d8..f9ae94b9 100644 --- a/src/buildservice/repository.py +++ b/src/buildservice/repository.py @@ -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() diff --git a/src/crontab/pakfire-build-service b/src/crontab/pakfire-build-service index e08f397f..7ad1516d 100644 --- a/src/crontab/pakfire-build-service +++ b/src/crontab/pakfire-build-service @@ -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 diff --git a/src/database.sql b/src/database.sql index 431d4159..c3f32820 100644 --- a/src/database.sql +++ b/src/database.sql @@ -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: - -- diff --git a/src/scripts/pakfire-build-service b/src/scripts/pakfire-build-service index 06aa0b77..8c0fa109 100644 --- a/src/scripts/pakfire-build-service +++ b/src/scripts/pakfire-build-service @@ -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