From 2c6c508643f8c2a9875686200b875de18ccc6b55 Mon Sep 17 00:00:00 2001 From: some Date: Mon, 30 Sep 2024 19:02:10 -0400 Subject: [PATCH] Initial release --- .editorconfig | 35 ++++++++ LICENSE | 25 ++++++ authentication/session.go | 106 +++++++++++++++++++++++ authorization/audit.go | 22 +++++ authorization/authorization.go | 43 ++++++++++ go.mod | 10 +++ go.sum | 6 ++ identity/identity.go | 120 ++++++++++++++++++++++++++ security.go | 45 ++++++++++ signature/signature.go | 149 +++++++++++++++++++++++++++++++++ 10 files changed, 561 insertions(+) create mode 100644 .editorconfig create mode 100644 LICENSE create mode 100644 authentication/session.go create mode 100644 authorization/audit.go create mode 100644 authorization/authorization.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 identity/identity.go create mode 100644 security.go create mode 100644 signature/signature.go diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..0cf80d3 --- /dev/null +++ b/.editorconfig @@ -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 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ed0e7b3 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/authentication/session.go b/authentication/session.go new file mode 100644 index 0000000..43ffde8 --- /dev/null +++ b/authentication/session.go @@ -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 +} diff --git a/authorization/audit.go b/authorization/audit.go new file mode 100644 index 0000000..ef47105 --- /dev/null +++ b/authorization/audit.go @@ -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 +} diff --git a/authorization/authorization.go b/authorization/authorization.go new file mode 100644 index 0000000..b3db05f --- /dev/null +++ b/authorization/authorization.go @@ -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 +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..0b0fb4f --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..602a263 --- /dev/null +++ b/go.sum @@ -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= diff --git a/identity/identity.go b/identity/identity.go new file mode 100644 index 0000000..cb190a4 --- /dev/null +++ b/identity/identity.go @@ -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 +} diff --git a/security.go b/security.go new file mode 100644 index 0000000..3a46c11 --- /dev/null +++ b/security.go @@ -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 +} diff --git a/signature/signature.go b/signature/signature.go new file mode 100644 index 0000000..d09647c --- /dev/null +++ b/signature/signature.go @@ -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 +}