replaced custom auth with existing methods

This commit is contained in:
Danila Fominykh 2023-07-23 15:42:52 +03:00
parent 245a54532f
commit 597a948e33
No known key found for this signature in database
GPG Key ID: 1134F8EBF98AA06F
5 changed files with 41 additions and 332 deletions

View File

@ -14,16 +14,12 @@ menu:
# Arch package registry
Gitea has arch package registry, which can act as a fully working [arch linux mirror](https://wiki.archlinux.org/title/mirrors) and connected directly in `/etc/pacman.conf`. Gitea automatically creates pacman database for packages in user space when new arch package is uploaded.
Gitea has arch package registry, which can act as a fully working [arch linux mirror](https://wiki.archlinux.org/title/mirrors) and connected directly in `/etc/pacman.conf`. Gitea automatically creates pacman database for packages in user/organization space when new arch package is uploaded.
**Table of Contents**
{{< toc >}}
## Requirements
You can install packages in any environment with [pacman](https://wiki.archlinux.org/title/Pacman). Alternatively you can use [pack](https://fmnx.su/core/pack) which connects specified registries automatically and provides simple interface for package uploads and deletions.
## Install packages
First, you need to update your pacman configuration, adding following lines:
@ -39,136 +35,37 @@ Then, you can run pacman sync command (with -y flag to load connected database f
pacman -Sy package
```
## GPG Verification
Upload and remove operation are validated with [GnuPG](https://gnupg.org/). First, you need to export and upload your public gpg key to `SSH/GPG Keys` in account settings. This works similarly to SSH keys. You can export gpg key with command:
```sh
gpg --armor --export
```
```
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQENBGSYoJUBCADSJ6v8Egst/gNJVC2206o8JqTzRBxTULKm/DH5J7AzrhJBxC2/
...
-----END PGP PUBLIC KEY BLOCK-----
```
## Upload packages
1. Ensure, that your package have been signed with your gpg key (more about arch [package signing](https://wiki.archlinux.org/title/DeveloperWiki:Package_signing)). You can do that by running following command:
```sh
gpg --verify package-ver-1-x86_64.pkg.tar.zst.sig
```
1. Sign message metadata, which consists of package owner (namespace in gitea), package file name and send time. You can do that by running following command:
```sh
echo -n {owner}{package}$(date --rfc-3339=seconds | tr " " T) >> md
gpg --detach-sign md
```
1. Decode message and metadata signatures to hex, by running following commands, save output somewhere.
```sh
xxd -p md.sig >> md.sig.hex
xxd -p package-1-1-x86_64.pkg.tar.zst.sig >> pkg.sig.hex
xxd -p package-1-1-x86_64.pkg.tar.zst.sig >> package-signature-hex
```
1. Paste your parameters and push package with [curl](https://curl.se/). Important, that time should be the same with metadata (signed md file), since this value is verified with GnuPG.
2. Paste your parameters and push package with [curl](https://curl.se/). Important, that time should be the same with metadata (signed md file), since this value is verified with GnuPG.
```sh
curl -X PUT \
'https://{domain}/api/packages/{owner}/arch/push' \
--header 'filename: {package}-1-1-x86_64.pkg.tar.zst' \
--header 'email: dancheg97@fmnx.su' \
--header 'filename: package-1-1-x86_64.pkg.tar.zst' \
--header 'distro: archlinux' \
--header 'time: {metadata-time}' \
--header 'pkgsign: {package-signature-hex}' \
--header 'metasign: {metadata-signature-hex}' \
--header 'Content-Type: application/octet-stream' \
--data-binary '@/path/to/package/file/{package}-1-1-x86_64.pkg.tar.zst'
```
Full script for package upload:
```sh
owner=user
package=package-0.1.0-1-x86_64.pkg.tar.zst
email=user@example.com
time=`date --rfc-3339=seconds | tr " " T`
pkgsignhex=`xxd -p $package.sig | tr -d "\n"`
echo -n $owner$package$time >> mddata
gpg --detach-sign mddata
mdsignhex=`xxd -p mddata.sig | tr -d "\n"`
curl -X PUT \
http://{domain}/api/packages/$owner/arch/push \
--header "filename: $package" \
--header "email: $email" \
--header "time: $time" \
--header "distro: archlinux" \
--header "metasign: $mdsignhex" \
--header "pkgsign: $pkgsignhex" \
--header 'Content-Type: application/octet-stream' \
--data-binary @$package
--data-binary '@/path/to/package/file/package-1-1-x86_64.pkg.tar.zst'
```
## Delete packages
1. Prepare signature for delete message.
```sh
echo -n {owner}{package}$(date --rfc-3339=seconds | tr " " T) >> md
gpg --detach-sign md
```
1. Send delete message with [curl](https://curl.se/). Time should be the same with saved in `md` file.
```sh
curl -X DELETE \
http://localhost:3000/api/packages/{user}/arch/remove \
--header "username: {user}" \
--header "email: user@email.com" \
--header "target: package" \
--header "time: {rmtime}" \
--header "version: {version-release}" \
--header 'Content-Type: application/octet-stream' \
--data-binary @md.sig
```
Full script for package deletion:
```sh
owner=user
package=package
version=0.1.0-1
email=user@example.com
arch=x86_64
time=`date --rfc-3339=seconds | tr " " T`
sudo rm -rf md md.sig
echo -n $owner$package$time >> md
gpg --detach-sign md
curl -X DELETE \
http://{domain}/api/packages/$owner/arch/remove \
--header "username: $owner" \
--header "email: $email" \
--header "target: $package" \
--header "time: $time" \
--header "version: $version" \
--header 'Content-Type: application/octet-stream' \
--data-binary @md.sig
--header "version: {version-release}"
```
## Clients
You can generate client code with tools like [thunder client](https://marketplace.visualstudio.com/items?itemName=rangav.vscode-thunder-client), [postman](https://blog.postman.com/curl-and-postman-work-wonderfully-together/) or other code generators to write your own client.
Also you can take a look at [pack](https://fmnx.su/core/pack) which provides `pacman` functionality with additional commands to build, sign and push your arch packages to gitea.
You can use gitea CLI tool to - [tea](https://gitea.com/gitea/tea) to push/remove arch packages from gitea. Alternatively, you can try [pack](https://fmnx.su/core/pack).

View File

@ -124,8 +124,8 @@ func CommonRoutes() *web.Route {
})
}, reqPackageAccess(perm.AccessModeRead))
r.Group("/arch", func() {
r.Put("/push", arch.Push)
r.Delete("/remove", arch.Remove)
r.Put("/push", arch.Push, reqPackageAccess(perm.AccessModeWrite))
r.Delete("/remove", arch.Remove, reqPackageAccess(perm.AccessModeWrite))
r.Get("/{distro}/{arch}/{file}", arch.Get)
})
r.Group("/cargo", func() {

View File

@ -23,20 +23,10 @@ func Push(ctx *context.Context) {
var (
owner = ctx.Params("username")
filename = ctx.Req.Header.Get("filename")
email = ctx.Req.Header.Get("email")
distro = ctx.Req.Header.Get("distro")
sendtime = ctx.Req.Header.Get("time")
pkgsign = ctx.Req.Header.Get("pkgsign")
metasign = ctx.Req.Header.Get("metasign")
sign = ctx.Req.Header.Get("sign")
)
// Decoding package signature.
sigdata, err := hex.DecodeString(pkgsign)
if err != nil {
apiError(ctx, http.StatusBadRequest, err)
return
}
// Read package to memory for signature validation.
pkgdata, err := io.ReadAll(ctx.Req.Body)
if err != nil {
@ -45,33 +35,6 @@ func Push(ctx *context.Context) {
}
defer ctx.Req.Body.Close()
// Get user and organization owning arch package.
user, org, err := arch_service.IdentifyOwner(ctx, owner, email)
if err != nil {
apiError(ctx, http.StatusUnauthorized, err)
return
}
// Decoding time when message was created.
t, err := time.Parse(time.RFC3339, sendtime)
if err != nil {
apiError(ctx, http.StatusBadRequest, err)
return
}
// Check if message is outdated.
if time.Since(t) > time.Hour {
apiError(ctx, http.StatusUnauthorized, "outdated message")
return
}
// Decoding signature related to metadata.
msigdata, err := hex.DecodeString(metasign)
if err != nil {
apiError(ctx, http.StatusBadRequest, err)
return
}
// Parse metadata contained in arch package archive.
md, err := arch_module.EjectMetadata(filename, distro, setting.Domain, pkgdata)
if err != nil {
@ -79,26 +42,10 @@ func Push(ctx *context.Context) {
return
}
// Validating metadata signature, to ensure that operation push operation
// is initiated by original package owner.
sendmetadata := []byte(owner + md.Name + sendtime)
err = arch_service.ValidateSignature(ctx, sendmetadata, msigdata, user)
if err != nil {
apiError(ctx, http.StatusUnauthorized, err)
return
}
// Validate package signature with any of user's GnuPG keys.
err = arch_service.ValidateSignature(ctx, pkgdata, sigdata, user)
if err != nil {
apiError(ctx, http.StatusUnauthorized, err)
return
}
// Save file related to arch package.
pkgid, err := arch_service.SaveFile(ctx, &arch_service.SaveFileParams{
Organization: org,
User: user,
Creator: ctx.Doer,
Owner: ctx.Package.Owner,
Metadata: md,
Filename: filename,
Data: pkgdata,
@ -109,10 +56,12 @@ func Push(ctx *context.Context) {
return
}
// Save file related to arch package signature.
// Decoding package signature, if present saving with package as file.
sigdata, err := hex.DecodeString(sign)
if err == nil {
_, err = arch_service.SaveFile(ctx, &arch_service.SaveFileParams{
Organization: org,
User: user,
Creator: ctx.Doer,
Owner: ctx.Package.Owner,
Metadata: md,
Data: sigdata,
Filename: filename + ".sig",
@ -122,10 +71,11 @@ func Push(ctx *context.Context) {
apiError(ctx, http.StatusInternalServerError, err)
return
}
}
// Add new architectures and distribution info to package version metadata.
err = arch_service.UpdateMetadata(ctx, &arch_service.UpdateMetadataParameters{
User: org.AsUser(),
User: ctx.Package.Owner,
Md: md,
})
if err != nil {
@ -190,50 +140,12 @@ func Get(ctx *context.Context) {
// Remove specific package version, related files and pacman database entry.
func Remove(ctx *context.Context) {
var (
owner = ctx.Params("username")
email = ctx.Req.Header.Get("email")
target = ctx.Req.Header.Get("target")
stime = ctx.Req.Header.Get("time")
version = ctx.Req.Header.Get("version")
pkg = ctx.Req.Header.Get("package")
ver = ctx.Req.Header.Get("version")
)
// Parse sent time and check if it is within last minute.
t, err := time.Parse(time.RFC3339, stime)
if err != nil {
apiError(ctx, http.StatusBadRequest, err)
return
}
if time.Since(t) > time.Minute {
apiError(ctx, http.StatusUnauthorized, "outdated message")
return
}
// Get user owning the package.
user, org, err := arch_service.IdentifyOwner(ctx, owner, email)
if err != nil {
apiError(ctx, http.StatusUnauthorized, err)
return
}
// Read signature data from request body.
sigdata, err := io.ReadAll(ctx.Req.Body)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
defer ctx.Req.Body.Close()
// Validate package signature with any of user's GnuPG keys.
mesdata := []byte(owner + target + stime)
err = arch_service.ValidateSignature(ctx, mesdata, sigdata, user)
if err != nil {
apiError(ctx, http.StatusUnauthorized, err)
return
}
// Remove package files and pacman database entry.
err = arch_service.RemovePackage(ctx, org.AsUser(), target, version)
err := arch_service.RemovePackage(ctx, ctx.Package.Owner, pkg, ver)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return

View File

@ -10,7 +10,6 @@ import (
"strings"
"code.gitea.io/gitea/models/db"
org_model "code.gitea.io/gitea/models/organization"
pkg_model "code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/user"
@ -58,9 +57,9 @@ func UpdateMetadata(ctx *context.Context, p *UpdateMetadataParameters) error {
// Parameters required to save new arch package.
type SaveFileParams struct {
*org_model.Organization
*user.User
*arch.Metadata
Creator *user.User
Owner *user.User
Metadata *arch.Metadata
Data []byte
Filename string
Distro string
@ -80,12 +79,12 @@ func SaveFile(ctx *context.Context, p *SaveFileParams) (int64, error) {
pv, _, err := pkg_service.CreatePackageOrAddFileToExisting(
&pkg_service.PackageCreationInfo{
PackageInfo: pkg_service.PackageInfo{
Owner: p.Organization.AsUser(),
Owner: p.Owner,
PackageType: pkg_model.TypeArch,
Name: p.Metadata.Name,
Version: p.Metadata.Version,
},
Creator: p.User,
Creator: p.Creator,
Metadata: p.Metadata,
},
&pkg_service.PackageFileCreationInfo{
@ -93,7 +92,7 @@ func SaveFile(ctx *context.Context, p *SaveFileParams) (int64, error) {
Filename: p.Filename,
CompositeKey: p.Distro + "-" + p.Filename,
},
Creator: p.User,
Creator: p.Creator,
Data: buf,
OverwriteExisting: true,
IsLead: p.IsLead,

View File

@ -1,99 +0,0 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package arch
import (
"bytes"
"errors"
"fmt"
"strings"
"code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/models/db"
org "code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
"github.com/keybase/go-crypto/openpgp"
)
type IdentidyOwnerParameters struct {
*context.Context
Owner string
Email string
}
// This function will find user related to provided email address and check if
// he is able to push packages to provided namespace (user/organization/or
// empty namespace allowed for admin users). Function will return user making
// operation, organization or user owning the package.
func IdentifyOwner(ctx *context.Context, owner, email string) (*user.User, *org.Organization, error) {
u, err := user.GetUserByEmail(ctx, email)
if err != nil {
return nil, nil, fmt.Errorf("unable to find user with email %s, %v", email, err)
}
if owner == "" && u.IsAdmin {
return u, (*org.Organization)(u), nil
}
if owner == u.Name {
return u, (*org.Organization)(u), nil
}
if u.Name != owner {
org, err := org.GetOrgByName(ctx, owner)
if err != nil {
return nil, nil, fmt.Errorf("unable to get organization: %s, %v", owner, err)
}
ismember, err := org.IsOrgMember(u.ID)
if err != nil {
return nil, nil, fmt.Errorf("unable to check if user %s belongs to organization %s: %v", u.Name, org.Name, err)
}
if !ismember {
return nil, nil, fmt.Errorf("user %s is not member of organization %s", u.Name, org.Name)
}
return u, org, nil
}
return nil, nil, fmt.Errorf("unknown package owner")
}
// Validate package signature with owner's GnuPG keys stored in gitea's database.
func ValidateSignature(ctx *context.Context, pkg, sign []byte, u *user.User) error {
keys, err := asymkey.ListGPGKeys(ctx, u.ID, db.ListOptions{
ListAll: true,
})
if err != nil {
return errors.New("unable to get public keys")
}
if len(keys) == 0 {
return errors.New("no keys for user with email: " + u.Email)
}
var keyarmors []string
for _, key := range keys {
k, err := asymkey.GetGPGImportByKeyID(key.KeyID)
if err != nil {
return errors.New("unable to import GPG key armor")
}
keyarmors = append(keyarmors, k.Content)
}
var trace []error
for _, armor := range keyarmors {
kr, err := openpgp.ReadArmoredKeyRing(strings.NewReader(armor))
if err != nil {
trace = append(trace, fmt.Errorf("unable to get keys for %s: %v", u.Name, err))
continue
}
_, err = openpgp.CheckDetachedSignature(kr, bytes.NewReader(pkg), bytes.NewReader(sign))
if err != nil {
trace = append(trace, err)
continue
}
return nil
}
return errors.Join(trace...)
}