Skip to content

Commit

Permalink
Add signature package for signing events and verifying signatures
Browse files Browse the repository at this point in the history
The new package basically contains two types, Signer and Verifier,
which, unsurprisingly, sign events and verifies signatures of existing
events.

Signer instances are configured with an identity and a private key
and signs events into byte slices. Verifier instance require you
to pass something that implements the PublicKeyLocator interface.
That interface looks up which public key(s) can be used to verify
the signature of an event with a given meta.security.authorIdentity.
Because public key lookups are expected to be application-dependent
we don't include a type that implements PublicKeyLocator, but that
might change over time once we understand typical usage patterns.
  • Loading branch information
magnusbaeck committed Jun 20, 2024
1 parent 3900560 commit e1791b6
Show file tree
Hide file tree
Showing 10 changed files with 1,039 additions and 0 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,14 @@ configuration burden, validator.DefaultSet returns a reasonably configured
validator.Set instance that's ready to be used. See the documentation of
the validator subpackage for details.

## Signing events and verifying signatures

The SDK supports cryptographic signing of (typically) outbound events as
well as verification of the signature of inbound events. The signing is
done according to the standard method, with the signature and metadata under
the `meta.security` field. See the documentation of the signature subpackage
for details and code examples.

## Code of Conduct and Contributing
To get involved, please see [Code of Conduct](https://github.com/eiffel-community/.github/blob/master/CODE_OF_CONDUCT.md) and [contribution guidelines](https://github.com/eiffel-community/.github/blob/master/CONTRIBUTING.md).

Expand Down
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@ require (
github.com/Showmax/go-fqdn v1.0.0
github.com/clarketm/json v1.17.1
github.com/gertd/go-pluralize v0.2.1
github.com/go-ldap/ldap v3.0.3+incompatible
github.com/google/renameio v1.0.1
github.com/google/uuid v1.6.0
github.com/gowebpki/jcs v1.0.1
github.com/lestrrat-go/jsschema v0.0.0-20181205002244-5c81c58ffcc3
github.com/package-url/packageurl-go v0.1.0
github.com/stretchr/testify v1.9.0
github.com/tidwall/gjson v1.17.1
github.com/tidwall/sjson v1.2.5
github.com/xeipuuv/gojsonschema v1.2.0
golang.org/x/text v0.15.0
gopkg.in/yaml.v3 v3.0.1
Expand All @@ -32,5 +35,6 @@ require (
github.com/tidwall/pretty v1.2.0 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
)
9 changes: 9 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,14 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gertd/go-pluralize v0.2.1 h1:M3uASbVjMnTsPb0PNqg+E/24Vwigyo/tvyMTtAlLgiA=
github.com/gertd/go-pluralize v0.2.1/go.mod h1:rbYaKDbsXxmRfr8uygAEKhOWsjyrrqrkHVpZvoOp8zk=
github.com/go-ldap/ldap v3.0.3+incompatible h1:HTeSZO8hWMS1Rgb2Ziku6b8a7qRIZZMHjsvuZyatzwk=
github.com/go-ldap/ldap v3.0.3+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc=
github.com/google/renameio v1.0.1 h1:Lh/jXZmvZxb0BBeSY5VKEfidcbcbenKjZFzM/q0fSeU=
github.com/google/renameio v1.0.1/go.mod h1:t/HQoYBZSsWSNK35C6CO/TpPLDVWvxOHboWUAweKUpk=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gowebpki/jcs v1.0.1 h1:Qjzg8EOkrOTuWP7DqQ1FbYtcpEbeTzUoTN9bptp8FOU=
github.com/gowebpki/jcs v1.0.1/go.mod h1:CID1cNZ+sHp1CCpAR8mPf6QRtagFBgPJE0FCUQ6+BrI=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
Expand Down Expand Up @@ -50,12 +54,15 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U=
github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
Expand Down Expand Up @@ -94,6 +101,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d h1:TxyelI5cVkbREznMhfzycHdkp5cLA7DpE+GKjSslYhM=
gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
Expand Down
31 changes: 31 additions & 0 deletions signature/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright Axis Communications AB.
//
// For a full list of individual contributors, please see the commit history.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package signature

import "errors"

var (
ErrMarshaling = errors.New("the marshaling of the event was unsuccessful")
ErrPublicKeyInvalid = errors.New("public key had the wrong type")
ErrPublicKeyLookup = errors.New("an error occurred looking up the public key for this identity")
ErrPublicKeyNotFound = errors.New("no public key for verifying events signed by this identify was found")
ErrSignatureMismatch = errors.New("the signature couldn't be verified")
ErrSigningFailed = errors.New("signing of the event failed")
ErrUnsupportedAlgorithm = errors.New("unsupported algorithm")
ErrUnverifiableEvent = errors.New("event cannot be verified because an essential field is unset or empty")
ErrVerificationFailed = errors.New("the signature couldn't be verified by any of the available public keys")
)
182 changes: 182 additions & 0 deletions signature/signer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
// Copyright Axis Communications AB.
//
// For a full list of individual contributors, please see the commit history.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package signature

import (
"crypto"
"crypto/ecdsa"
"crypto/rand"
"crypto/rsa"
"encoding/base64"
"encoding/json"
"errors"
"fmt"

"github.com/gowebpki/jcs"
"github.com/tidwall/sjson"

"github.com/eiffel-community/eiffelevents-sdk-go"
)

// Algorithm describes the set of algorithms used when signing or verifying a payload.
// It includes both the signing algorithm and the hash algorithm. The latter is
// important since it's the hash that's signed rather than the payload itself.
type Algorithm string

const (
RS256 = Algorithm(eiffelevents.MetaV3SecurityIntegrityProtectionAlg_RS256) // RSASSA-PKCS1-v1_5 using SHA-256
RS384 = Algorithm(eiffelevents.MetaV3SecurityIntegrityProtectionAlg_RS384) // RSASSA-PKCS1-v1_5 using SHA-384
RS512 = Algorithm(eiffelevents.MetaV3SecurityIntegrityProtectionAlg_RS512) // RSASSA-PKCS1-v1_5 using SHA-512
ES256 = Algorithm(eiffelevents.MetaV3SecurityIntegrityProtectionAlg_ES256) // ECDSA using P-256 and SHA-256
ES384 = Algorithm(eiffelevents.MetaV3SecurityIntegrityProtectionAlg_ES384) // ECDSA using P-384 and SHA-384
ES512 = Algorithm(eiffelevents.MetaV3SecurityIntegrityProtectionAlg_ES512) // ECDSA using P-521 and SHA-512
PS256 = Algorithm(eiffelevents.MetaV3SecurityIntegrityProtectionAlg_PS256) // RSASSA-PSS using SHA-256 and MGF1 with SHA-256
PS384 = Algorithm(eiffelevents.MetaV3SecurityIntegrityProtectionAlg_PS384) // RSASSA-PSS using SHA-384 and MGF1 with SHA-384
PS512 = Algorithm(eiffelevents.MetaV3SecurityIntegrityProtectionAlg_PS512) // RSASSA-PSS using SHA-512 and MGF1 with SHA-512
)

const (
authorIdentityField = "meta.security.authorIdentity"
algorithmField = "meta.security.integrityProtection.alg"
signatureField = "meta.security.integrityProtection.signature"
)

// Signer signs Eiffel event payloads in the standard way.
type Signer struct {
identity string
alg Algorithm
pk crypto.PrivateKey
hashFunc func([]byte) []byte
signFunc func(crypto.PrivateKey, crypto.Hash, []byte) ([]byte, error)
signerOpts crypto.SignerOpts
}

// NewKeySigner initializes a Signer with a private key and an identity.
func NewKeySigner(identity string, alg Algorithm, pk crypto.PrivateKey) (*Signer, error) {
s := &Signer{
identity: identity,
alg: alg,
pk: pk,
}
switch s.alg {
case RS256:
s.hashFunc = hashSHA256
s.signerOpts = crypto.SHA256
s.signFunc = signPKCS1v15
case ES256:
s.hashFunc = hashSHA256
s.signerOpts = crypto.SHA256
s.signFunc = signECDSA
case PS256:
s.hashFunc = hashSHA256
s.signerOpts = crypto.SHA256
s.signFunc = signPSS
case RS384:
s.hashFunc = hashSHA384
s.signerOpts = crypto.SHA384
s.signFunc = signPKCS1v15
case ES384:
s.hashFunc = hashSHA384
s.signerOpts = crypto.SHA384
s.signFunc = signECDSA
case PS384:
s.hashFunc = hashSHA384
s.signerOpts = crypto.SHA384
s.signFunc = signPSS
case RS512:
s.hashFunc = hashSHA512
s.signerOpts = crypto.SHA512
s.signFunc = signPKCS1v15
case ES512:
s.hashFunc = hashSHA512
s.signerOpts = crypto.SHA512
s.signFunc = signECDSA
case PS512:
s.hashFunc = hashSHA512
s.signerOpts = crypto.SHA512
s.signFunc = signPSS
default:
return nil, fmt.Errorf("%w: %s", ErrUnsupportedAlgorithm, s.alg)
}
return s, nil
}

// Sign signs the provided event and returns it as a byte slice that includes
// the signature itself and the details needed to verify the signature.
//
// - If something goes wrong while modifying the event in preparation of
// the signing, ErrMarshaling is returned.
// - If the signing itself fails, ErrSigningFailed is returned.
//
// Errors will be returned in wrapped form so make sure you use errors.Is
// rather than direct comparisons.
func (s *Signer) Sign(event json.Marshaler) ([]byte, error) {
// TODO: Check if meta.security is supported in this event version.

eventBytes, err := event.MarshalJSON()
if err != nil {
return nil, errors.Join(ErrMarshaling, err)

Check failure on line 132 in signature/signer.go

View workflow job for this annotation

GitHub Actions / run-tests

undefined: errors.Join (typecheck)
}

// Set the signing-related fields, make sure there's an empty signature field,
// and transform the event to canonical JSON.
if eventBytes, err = sjson.SetBytes(eventBytes, authorIdentityField, s.identity); err != nil {
return nil, errors.Join(ErrMarshaling, err)

Check failure on line 138 in signature/signer.go

View workflow job for this annotation

GitHub Actions / run-tests

undefined: errors.Join (typecheck)
}
if eventBytes, err = sjson.SetBytes(eventBytes, algorithmField, s.alg); err != nil {
return nil, errors.Join(ErrMarshaling, err)

Check failure on line 141 in signature/signer.go

View workflow job for this annotation

GitHub Actions / run-tests

undefined: errors.Join (typecheck)
}
if eventBytes, err = sjson.SetBytes(eventBytes, signatureField, ""); err != nil {
return nil, errors.Join(ErrMarshaling, err)
}
if eventBytes, err = jcs.Transform(eventBytes); err != nil {
return nil, errors.Join(ErrMarshaling, err)
}

sig, err := s.signFunc(s.pk, s.signerOpts.HashFunc(), s.hashFunc(eventBytes))
if err != nil {
return nil, errors.Join(ErrSigningFailed, err)
}
if eventBytes, err = sjson.SetBytes(eventBytes, signatureField, base64.StdEncoding.EncodeToString(sig)); err != nil {
return nil, errors.Join(ErrMarshaling, err)
}
return eventBytes, nil
}

func signECDSA(priv crypto.PrivateKey, hash crypto.Hash, digest []byte) ([]byte, error) {
privECDSA, ok := priv.(*ecdsa.PrivateKey)
if !ok {
return nil, fmt.Errorf("private key had the wrong type; expected *ecdsa.PrivateKey, got %T", priv)
}
return ecdsa.SignASN1(rand.Reader, privECDSA, digest)
}

func signPKCS1v15(priv crypto.PrivateKey, hash crypto.Hash, digest []byte) ([]byte, error) {
privRSA, ok := priv.(*rsa.PrivateKey)
if !ok {
return nil, fmt.Errorf("private key had the wrong type; expected *rsa.PrivateKey, got %T", priv)
}
return rsa.SignPKCS1v15(rand.Reader, privRSA, hash, digest)
}

func signPSS(priv crypto.PrivateKey, hash crypto.Hash, digest []byte) ([]byte, error) {
privRSA, ok := priv.(*rsa.PrivateKey)
if !ok {
return nil, fmt.Errorf("private key had the wrong type; expected *rsa.PrivateKey, got %T", priv)
}
return rsa.SignPSS(rand.Reader, privRSA, hash, digest, nil)
}
Loading

0 comments on commit e1791b6

Please sign in to comment.