import datetime
import io
import logging
+import pakfire
from . import base
# 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("""
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,
)
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
# 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)
# 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)
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)
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
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
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)
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"
"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,
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):
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")
# 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)
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)
if user:
self._set_attribute("deleted_by", user)
+ # Delete the key
+ if self.key:
+ self.key.delete()
+
# Local path
path = self.local_path()
# 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
);
+--
+-- 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: -
--
);
---
--- 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: -
--
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
);
--
CREATE SEQUENCE public.keys_id_seq
+ AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
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: -
--
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: -
--
--
--- 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);
--
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: -
--
# Jobs
"jobs:installcheck" : self._jobs_installcheck,
- # Keys
- "keys:generate" : self.backend.keys.generate,
-
# Messages
"messages:queue:send" : self.backend.messages.queue.send,
# 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