Initial release
This commit is contained in:
commit
2c6c508643
35
.editorconfig
Normal file
35
.editorconfig
Normal file
@ -0,0 +1,35 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
|
||||
end_of_line = LF
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.sh]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[{*.html,*.js,*.css,*.scss}]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
indent_size = 8
|
||||
|
||||
[{{*.,}[Dd]ockerfile{.*,},{*.,}[Cc]ontainerfile{.*,}}]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[*.proto]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[{*.go,go.mod}]
|
||||
indent_style = tab
|
||||
indent_size = 8
|
25
LICENSE
Normal file
25
LICENSE
Normal file
@ -0,0 +1,25 @@
|
||||
BSD 2-Clause License
|
||||
|
||||
Copyright (c) 2024, some
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
106
authentication/session.go
Normal file
106
authentication/session.go
Normal file
@ -0,0 +1,106 @@
|
||||
package authentication
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"somehole.com/common/security/identity"
|
||||
)
|
||||
|
||||
const (
|
||||
RetryCreateSessionToken = 3
|
||||
MaxSessionAge = 1 * time.Hour
|
||||
)
|
||||
|
||||
type SessionToken [8]byte
|
||||
|
||||
func NewSessionToken() (st SessionToken) {
|
||||
var stBytes = make([]byte, 8)
|
||||
rand.Read(stBytes)
|
||||
st = SessionToken(stBytes)
|
||||
return
|
||||
}
|
||||
|
||||
type NextToken [4]byte
|
||||
|
||||
func NewNextToken() (nt NextToken) {
|
||||
var ntBytes = make([]byte, 4)
|
||||
rand.Read(ntBytes)
|
||||
nt = NextToken(ntBytes)
|
||||
return
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
identity.Identity
|
||||
nextTokens []NextToken
|
||||
expiration time.Time
|
||||
}
|
||||
|
||||
func NewSession(identity identity.Identity) (s *Session) {
|
||||
s = &Session{
|
||||
Identity: identity,
|
||||
nextTokens: make([]NextToken, 0),
|
||||
expiration: time.Now().Add(MaxSessionAge),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Session) Active() (ok bool) {
|
||||
ok = time.Now().Before(s.expiration)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Session) NewNextToken() (nt NextToken) {
|
||||
nt = NewNextToken()
|
||||
s.nextTokens = append(s.nextTokens, nt)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Session) NextTokenIsCurrent(nt NextToken) (ok bool) {
|
||||
ok = s.nextTokens[len(s.nextTokens)-1] == nt
|
||||
return
|
||||
}
|
||||
|
||||
type SessionService struct {
|
||||
sessions map[SessionToken]*Session
|
||||
}
|
||||
|
||||
func NewSessionService() (srv *SessionService) {
|
||||
srv = &SessionService{
|
||||
sessions: make(map[SessionToken]*Session),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (srv *SessionService) NewSessionToken() (st SessionToken, err error) {
|
||||
for i := 0; i < RetryCreateSessionToken; i++ {
|
||||
st = NewSessionToken()
|
||||
if _, exists := srv.sessions[st]; exists {
|
||||
err = fmt.Errorf("could only creat colliding session tokens in %d tries", RetryCreateSessionToken)
|
||||
continue
|
||||
}
|
||||
err = nil
|
||||
break
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (srv *SessionService) AddSession(st SessionToken, session *Session) (err error) {
|
||||
if _, exists := srv.sessions[st]; exists {
|
||||
err = fmt.Errorf("session already exists")
|
||||
return
|
||||
}
|
||||
srv.sessions[st] = session
|
||||
return
|
||||
}
|
||||
|
||||
func (srv *SessionService) GetSession(sessionToken SessionToken) (session *Session, err error) {
|
||||
var exists bool
|
||||
session, exists = srv.sessions[sessionToken]
|
||||
if !exists {
|
||||
err = fmt.Errorf("session not found")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
22
authorization/audit.go
Normal file
22
authorization/audit.go
Normal file
@ -0,0 +1,22 @@
|
||||
package authorization
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"somehole.com/common/security/identity"
|
||||
)
|
||||
|
||||
type record struct {
|
||||
Verb
|
||||
Noun
|
||||
time.Time
|
||||
}
|
||||
|
||||
type AuditService struct {
|
||||
records map[identity.Id][]record
|
||||
}
|
||||
|
||||
func NewAuditService() (srv *AuditService) {
|
||||
srv = &AuditService{}
|
||||
return
|
||||
}
|
43
authorization/authorization.go
Normal file
43
authorization/authorization.go
Normal file
@ -0,0 +1,43 @@
|
||||
package authorization
|
||||
|
||||
import "somehole.com/common/security/identity"
|
||||
|
||||
type Verb int
|
||||
|
||||
const (
|
||||
_ Verb = iota
|
||||
Create
|
||||
Read
|
||||
Watch
|
||||
Update
|
||||
Patch
|
||||
Delete
|
||||
)
|
||||
|
||||
type Noun int
|
||||
|
||||
const (
|
||||
_ Noun = iota
|
||||
Identity
|
||||
Command
|
||||
Log
|
||||
)
|
||||
|
||||
type AuthorizationService struct {
|
||||
*AuditService
|
||||
}
|
||||
|
||||
func NewAuthorizationService(auditService *AuditService) (srv *AuthorizationService) {
|
||||
if auditService == nil {
|
||||
auditService = NewAuditService()
|
||||
}
|
||||
srv = &AuthorizationService{
|
||||
AuditService: auditService,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (srv *AuthorizationService) Authorized(identity identity.Identity, verb Verb, noun Noun) (authorized bool, err error) {
|
||||
authorized = true
|
||||
return
|
||||
}
|
10
go.mod
Normal file
10
go.mod
Normal file
@ -0,0 +1,10 @@
|
||||
module somehole.com/common/security
|
||||
|
||||
go 1.23.1
|
||||
|
||||
require github.com/cloudflare/circl v1.4.0
|
||||
|
||||
require (
|
||||
golang.org/x/crypto v0.11.1-0.20230711161743-2e82bdd1719d // indirect
|
||||
golang.org/x/sys v0.10.0 // indirect
|
||||
)
|
6
go.sum
Normal file
6
go.sum
Normal file
@ -0,0 +1,6 @@
|
||||
github.com/cloudflare/circl v1.4.0 h1:BV7h5MgrktNzytKmWjpOtdYrf0lkkbF8YMlBGPhJQrY=
|
||||
github.com/cloudflare/circl v1.4.0/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
|
||||
golang.org/x/crypto v0.11.1-0.20230711161743-2e82bdd1719d h1:LiA25/KWKuXfIq5pMIBq1s5hz3HQxhJJSu/SUGlD+SM=
|
||||
golang.org/x/crypto v0.11.1-0.20230711161743-2e82bdd1719d/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
||||
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
120
identity/identity.go
Normal file
120
identity/identity.go
Normal file
@ -0,0 +1,120 @@
|
||||
package identity
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"somehole.com/common/security/signature"
|
||||
)
|
||||
|
||||
const (
|
||||
RetryCreateId = 3
|
||||
)
|
||||
|
||||
type identity interface {
|
||||
GetId() []byte
|
||||
GetPublicKey() []byte
|
||||
GetPublicKeySignature() []byte
|
||||
}
|
||||
|
||||
type Id [8]byte
|
||||
|
||||
func (i Id) String() (out string) {
|
||||
out = base64.StdEncoding.EncodeToString(i[:])
|
||||
return
|
||||
}
|
||||
|
||||
type Identity struct {
|
||||
Id Id
|
||||
PublicKey signature.PublicKey
|
||||
PublicKeySignature signature.Signature
|
||||
}
|
||||
|
||||
func NewIdentity(ident identity) (i Identity, err error) {
|
||||
id := ident.GetId()
|
||||
if len(id) != 8 {
|
||||
err = errors.New("wrong size id")
|
||||
return
|
||||
}
|
||||
pubKey := ident.GetPublicKey()
|
||||
if len(pubKey) != 96 {
|
||||
err = errors.New("wrong size public key")
|
||||
return
|
||||
}
|
||||
pubKeySig := ident.GetPublicKeySignature()
|
||||
if len(pubKeySig) != 48 {
|
||||
err = errors.New("wrong size public key signature")
|
||||
return
|
||||
}
|
||||
i = Identity{
|
||||
Id: Id(id),
|
||||
PublicKey: signature.PublicKey(pubKey),
|
||||
PublicKeySignature: signature.Signature(pubKeySig),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (i Identity) String() (out string) {
|
||||
b := make([]byte, 0)
|
||||
b = append(b, i.Id[:]...)
|
||||
b = append(b, i.PublicKey[:]...)
|
||||
b = append(b, i.PublicKeySignature[:]...)
|
||||
out = base64.StdEncoding.EncodeToString(b)
|
||||
return
|
||||
}
|
||||
|
||||
func ParseIdentityString(in string) (i Identity, err error) {
|
||||
var b []byte
|
||||
b, err = base64.StdEncoding.DecodeString(in)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(b) != 8+96+48 {
|
||||
err = errors.New("wrong for size identity data in string")
|
||||
return
|
||||
}
|
||||
i = Identity{
|
||||
Id: Id(b[0:8]),
|
||||
PublicKey: signature.PublicKey(b[8:104]),
|
||||
PublicKeySignature: signature.Signature(b[104:152]),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type IdentityService struct {
|
||||
identities map[Id]Identity
|
||||
}
|
||||
|
||||
func NewIdentityService() (srv *IdentityService) {
|
||||
srv = &IdentityService{
|
||||
identities: make(map[Id]Identity),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (srv *IdentityService) NewId() (id Id, err error) {
|
||||
var idBytes = make([]byte, 8)
|
||||
for i := 0; i < RetryCreateId; i++ {
|
||||
rand.Read(idBytes)
|
||||
if _, exists := srv.identities[id]; exists {
|
||||
err = fmt.Errorf("could only creat colliding ids in %d tries", RetryCreateId)
|
||||
continue
|
||||
}
|
||||
id = Id(idBytes)
|
||||
err = nil
|
||||
break
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (srv *IdentityService) AddIdentity(id Id, identity Identity) (err error) {
|
||||
_, exists := srv.identities[id]
|
||||
if exists {
|
||||
err = fmt.Errorf("failed to add identity")
|
||||
return
|
||||
}
|
||||
srv.identities[id] = identity
|
||||
return
|
||||
}
|
45
security.go
Normal file
45
security.go
Normal file
@ -0,0 +1,45 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"somehole.com/common/security/authentication"
|
||||
"somehole.com/common/security/authorization"
|
||||
"somehole.com/common/security/identity"
|
||||
"somehole.com/common/security/signature"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Signer signature.Keypair
|
||||
}
|
||||
|
||||
type SecurityService struct {
|
||||
config Config
|
||||
*authentication.SessionService
|
||||
*authorization.AuditService
|
||||
*authorization.AuthorizationService
|
||||
*identity.IdentityService
|
||||
*signature.SignatureService
|
||||
}
|
||||
|
||||
func NewSecurityService(config ...Config) (srv *SecurityService) {
|
||||
cfg := Config{}
|
||||
if len(config) == 1 {
|
||||
cfg = config[0]
|
||||
}
|
||||
signatureService, err := signature.NewSignatureService(&cfg.Signer)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
identityService := identity.NewIdentityService()
|
||||
sessionService := authentication.NewSessionService()
|
||||
auditService := authorization.NewAuditService()
|
||||
authorizationService := authorization.NewAuthorizationService(auditService)
|
||||
srv = &SecurityService{
|
||||
config: cfg,
|
||||
SessionService: sessionService,
|
||||
AuditService: auditService,
|
||||
AuthorizationService: authorizationService,
|
||||
IdentityService: identityService,
|
||||
SignatureService: signatureService,
|
||||
}
|
||||
return
|
||||
}
|
149
signature/signature.go
Normal file
149
signature/signature.go
Normal file
@ -0,0 +1,149 @@
|
||||
package signature
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
|
||||
"github.com/cloudflare/circl/sign/bls"
|
||||
)
|
||||
|
||||
type PrivateKey [32]byte
|
||||
|
||||
func (pk PrivateKey) Bls() (privKey *bls.PrivateKey[bls.KeyG2SigG1], err error) {
|
||||
privKey = &bls.PrivateKey[bls.KeyG2SigG1]{}
|
||||
err = privKey.UnmarshalBinary(pk[:])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (pk PrivateKey) Sign(msg []byte) (sig Signature, err error) {
|
||||
privKey, err := pk.Bls()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
sig = Signature(bls.Sign(privKey, msg))
|
||||
return
|
||||
}
|
||||
|
||||
func (pk PrivateKey) SignKey(key PublicKey) (sig Signature, err error) {
|
||||
pubKey := &bls.PublicKey[bls.KeyG2SigG1]{}
|
||||
err = pubKey.UnmarshalBinary(key[:])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return pk.Sign(key[:])
|
||||
}
|
||||
|
||||
func (pk PrivateKey) String() (out string) {
|
||||
out = base64.StdEncoding.EncodeToString(pk[:])
|
||||
return
|
||||
}
|
||||
|
||||
type PublicKey [96]byte
|
||||
|
||||
func (pk PublicKey) Bls() (pubKey *bls.PublicKey[bls.KeyG2SigG1], err error) {
|
||||
pubKey = &bls.PublicKey[bls.KeyG2SigG1]{}
|
||||
err = pubKey.UnmarshalBinary(pk[:])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (pk PublicKey) Verify(msg []byte, sig Signature) (ok bool, err error) {
|
||||
pubKey, err := pk.Bls()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ok = bls.Verify(pubKey, msg, sig[:])
|
||||
return
|
||||
}
|
||||
|
||||
func (pk PublicKey) String() (out string) {
|
||||
out = base64.StdEncoding.EncodeToString(pk[:])
|
||||
return
|
||||
}
|
||||
|
||||
type Signature [48]byte
|
||||
|
||||
type Keypair struct {
|
||||
PrivateKey
|
||||
PublicKey
|
||||
}
|
||||
|
||||
func NewKeypair() (kp *Keypair, err error) {
|
||||
var (
|
||||
ikm = make([]byte, 64)
|
||||
salt = make([]byte, 32)
|
||||
privKey *bls.PrivateKey[bls.KeyG2SigG1]
|
||||
pubKey *bls.PublicKey[bls.KeyG2SigG1]
|
||||
privKeyBytes = make([]byte, 32)
|
||||
pubKeyBytes = make([]byte, 96)
|
||||
)
|
||||
_, err = rand.Read(ikm)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, err = rand.Read(salt)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
privKey, err = bls.KeyGen[bls.KeyG2SigG1](ikm, salt, []byte(""))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
privKeyBytes, err = privKey.MarshalBinary()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
pubKey = privKey.PublicKey()
|
||||
pubKeyBytes, err = pubKey.MarshalBinary()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
kp = &Keypair{
|
||||
PrivateKey: PrivateKey(privKeyBytes),
|
||||
PublicKey: PublicKey(pubKeyBytes),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (kp *Keypair) validate() (ok bool, err error) {
|
||||
var testMessage [128]byte
|
||||
_, err = rand.Read(testMessage[:])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
sig, _ := kp.PrivateKey.Sign(testMessage[:])
|
||||
ok, _ = kp.PublicKey.Verify(testMessage[:], sig)
|
||||
return
|
||||
}
|
||||
|
||||
type SignatureService struct {
|
||||
*Keypair
|
||||
}
|
||||
|
||||
func NewSignatureService(signer *Keypair) (srv *SignatureService, err error) {
|
||||
if signer == nil {
|
||||
var ok bool
|
||||
signer, err = NewKeypair()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ok, err = signer.validate()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
err = errors.New("failed to validate new signer keypair")
|
||||
return
|
||||
}
|
||||
}
|
||||
srv = &SignatureService{
|
||||
Keypair: signer,
|
||||
}
|
||||
return
|
||||
}
|
Loading…
Reference in New Issue
Block a user