From e9ac7d3a13639ea3a4f0b37f59da394f1caf3de9 Mon Sep 17 00:00:00 2001 From: Thibault NORMAND Date: Wed, 11 Oct 2023 18:30:18 +0200 Subject: [PATCH] Feat hpke dhkem (#322) * feat(hpke): add HPKE primitives. * chore(ci): golangci lint. --- cmd/harp/internal/cmd/transform_decompress.go | 4 +- pkg/bundle/codec.go | 2 +- pkg/sdk/fsutil/targzfs/builders.go | 4 +- pkg/sdk/fsutil/targzfs/fs.go | 4 +- pkg/sdk/ioutil/copy.go | 42 +- pkg/sdk/security/crypto/hpke/api.go | 217 ++++++++++ pkg/sdk/security/crypto/hpke/keyschedule.go | 190 +++++++++ pkg/sdk/security/crypto/hpke/receiver.go | 93 +++++ pkg/sdk/security/crypto/hpke/sender.go | 135 +++++++ pkg/sdk/security/crypto/hpke/suite.go | 88 ++++ pkg/sdk/security/crypto/hpke/vector_test.go | 250 ++++++++++++ pkg/sdk/security/crypto/kem/api.go | 92 +++++ pkg/sdk/security/crypto/kem/dhkem.go | 376 ++++++++++++++++++ pkg/sdk/security/crypto/kem/dhkem_test.go | 69 ++++ pkg/sdk/security/crypto/kem/doc.go | 7 + pkg/sdk/security/crypto/kem/key_derivation.go | 70 ++++ .../crypto/kem/key_derivation_test.go | 56 +++ pkg/sdk/security/crypto/paseto/v4/helpers.go | 362 ----------------- .../security/crypto/paseto/v4/helpers_test.go | 358 ----------------- pkg/sdk/value/encryption/age/transformer.go | 4 +- .../value/encryption/paseto/transformer.go | 11 +- 21 files changed, 1686 insertions(+), 748 deletions(-) create mode 100644 pkg/sdk/security/crypto/hpke/api.go create mode 100644 pkg/sdk/security/crypto/hpke/keyschedule.go create mode 100644 pkg/sdk/security/crypto/hpke/receiver.go create mode 100644 pkg/sdk/security/crypto/hpke/sender.go create mode 100644 pkg/sdk/security/crypto/hpke/suite.go create mode 100644 pkg/sdk/security/crypto/hpke/vector_test.go create mode 100644 pkg/sdk/security/crypto/kem/api.go create mode 100644 pkg/sdk/security/crypto/kem/dhkem.go create mode 100644 pkg/sdk/security/crypto/kem/dhkem_test.go create mode 100644 pkg/sdk/security/crypto/kem/doc.go create mode 100644 pkg/sdk/security/crypto/kem/key_derivation.go create mode 100644 pkg/sdk/security/crypto/kem/key_derivation_test.go delete mode 100644 pkg/sdk/security/crypto/paseto/v4/helpers.go delete mode 100644 pkg/sdk/security/crypto/paseto/v4/helpers_test.go diff --git a/cmd/harp/internal/cmd/transform_decompress.go b/cmd/harp/internal/cmd/transform_decompress.go index 6eb4611c..122e234c 100644 --- a/cmd/harp/internal/cmd/transform_decompress.go +++ b/cmd/harp/internal/cmd/transform_decompress.go @@ -80,10 +80,10 @@ var transformDecompressCmd = func() *cobra.Command { } // Compute max decompression size - maxDecompressionSize := int64(params.maxDecompressionGuard) * 1024 * 1024 + maxDecompressionSize := uint64(params.maxDecompressionGuard) * 1024 * 1024 // Process input as a stream. - if err := ioutil.Copy(maxDecompressionSize, writer, compressedReader); err != nil { + if _, err := ioutil.LimitCopy(writer, compressedReader, maxDecompressionSize); err != nil { log.SafeClose(compressedReader, "unable to close the compression writer") log.For(ctx).Fatal("unable to process input", zap.Error(err)) } diff --git a/pkg/bundle/codec.go b/pkg/bundle/codec.go index a3615feb..4a12705e 100644 --- a/pkg/bundle/codec.go +++ b/pkg/bundle/codec.go @@ -37,7 +37,7 @@ func Load(r io.Reader) (*bundlev1.Bundle, error) { // Use buffered copy decoded := &bytes.Buffer{} - if err = ioutil.Copy(maxBundleSize, decoded, r); err != nil { + if _, err = ioutil.LimitCopy(decoded, r, maxBundleSize); err != nil { return nil, fmt.Errorf("unable to load bundle content") } diff --git a/pkg/sdk/fsutil/targzfs/builders.go b/pkg/sdk/fsutil/targzfs/builders.go index bdc5b6db..07ecf4ce 100644 --- a/pkg/sdk/fsutil/targzfs/builders.go +++ b/pkg/sdk/fsutil/targzfs/builders.go @@ -46,7 +46,7 @@ func FromReader(r io.Reader) (fs.FS, error) { // Chunked read with hard limit to prevent/reduce zipbomb vulnerability // exploitation. - if err := ioutil.Copy(maxDecompressedSize, &tarContents, gz); err != nil { + if _, err := ioutil.LimitCopy(&tarContents, gz, maxDecompressedSize); err != nil { return nil, fmt.Errorf("unable to decompress the archive: %w", err) } @@ -91,7 +91,7 @@ func FromReader(r io.Reader) (fs.FS, error) { // Chunked read with hard limit to prevent/reduce post decompression // explosion - if err := ioutil.Copy(maxFileSize, &fileContents, tarReader); err != nil { + if _, err := ioutil.LimitCopy(&fileContents, tarReader, maxFileSize); err != nil { return nil, fmt.Errorf("unable to copy file content to memory: %w", err) } diff --git a/pkg/sdk/fsutil/targzfs/fs.go b/pkg/sdk/fsutil/targzfs/fs.go index 64da7b81..7a5e06ed 100644 --- a/pkg/sdk/fsutil/targzfs/fs.go +++ b/pkg/sdk/fsutil/targzfs/fs.go @@ -17,9 +17,9 @@ import ( var ( // Block decompression if the TAR archive is larger than 25MB. - maxDecompressedSize = int64(25 * 1024 * 1024) + maxDecompressedSize = uint64(25 * 1024 * 1024) // Maximum file size to load in memory (2MB). - maxFileSize = int64(2 * 1024 * 1024) + maxFileSize = uint64(2 * 1024 * 1024) // Block decompression if the archive has more than 1k files. maxFileCount = 1000 ) diff --git a/pkg/sdk/ioutil/copy.go b/pkg/sdk/ioutil/copy.go index 85628e53..053d43c3 100644 --- a/pkg/sdk/ioutil/copy.go +++ b/pkg/sdk/ioutil/copy.go @@ -7,37 +7,51 @@ package ioutil import ( "errors" + "fmt" "io" + "os" ) // ErrTruncatedCopy is raised when the copy is larger than expected. var ErrTruncatedCopy = errors.New("truncated copy due to too large input") -// Copy uses a buffered CopyN and a hardlimit to stop read from the reader when -// the maxSize amount of data has been written to the given writer. -func Copy(maxSize int64, w io.Writer, r io.Reader) error { - contentLength := int64(0) +// LimitCopy uses a buffered CopyN and a hardlimit to stop read from the reader when +// the maxSize amount of data has been written to the given writer and raise an +// error. +func LimitCopy(dst io.Writer, src io.Reader, maxSize uint64) (uint64, error) { + writtenLength := uint64(0) - // Chunked read with hard limit to prevent/reduce zipbomb vulnerability - // exploitation. + // Check arguments + if dst == nil { + return 0, errors.New("writer must not be nil") + } + if src == nil { + return 0, errors.New("reader must not be nil") + } + + // Retrieve system pagesize for optimized buffer length + pageSize := os.Getpagesize() + + // Chunked read with hard limit to reduce/prevent memory bomb. for { - written, err := io.CopyN(w, r, 1024) + written, err := io.CopyN(dst, src, int64(pageSize)) if err != nil { if errors.Is(err, io.EOF) { + writtenLength += uint64(written) break } - return err + return writtenLength, fmt.Errorf("unable to stream source data to destination: %w", err) } // Add to length - contentLength += written + writtenLength += uint64(written) + } - // Check max size - if contentLength > maxSize { - return ErrTruncatedCopy - } + // Check max size + if writtenLength > maxSize { + return writtenLength, ErrTruncatedCopy } // No error - return nil + return writtenLength, nil } diff --git a/pkg/sdk/security/crypto/hpke/api.go b/pkg/sdk/security/crypto/hpke/api.go new file mode 100644 index 00000000..d8472661 --- /dev/null +++ b/pkg/sdk/security/crypto/hpke/api.go @@ -0,0 +1,217 @@ +// SPDX-FileCopyrightText: 2019-2023 Thibault NORMAND +// +// SPDX-License-Identifier: Apache-2.0 AND MIT + +// Package hpke provides RFC9180 hybrid public key encryption features. +package hpke + +import ( + "crypto" + "crypto/aes" + "crypto/cipher" + "crypto/sha256" + "crypto/sha512" + "errors" + "fmt" + "hash" + "io" + + "golang.org/x/crypto/chacha20poly1305" + "golang.org/x/crypto/hkdf" + "zntr.io/harp/v2/pkg/sdk/security/crypto/kem" +) + +type mode uint8 + +const ( + modeBase mode = 0x00 + modePsk mode = 0x01 + modeAuth mode = 0x02 + modeAuthPsk mode = 0x03 +) + +// ----------------------------------------------------------------------------- + +type KEM uint16 + +//nolint:stylecheck +const ( + // KEM_P256_HKDF_SHA256 is a KEM using P-256 curve and HKDF with SHA-256. + KEM_P256_HKDF_SHA256 KEM = 0x10 + // KEM_P384_HKDF_SHA384 is a KEM using P-384 curve and HKDF with SHA-384. + KEM_P384_HKDF_SHA384 KEM = 0x11 + // KEM_P521_HKDF_SHA512 is a KEM using P-521 curve and HKDF with SHA-512. + KEM_P521_HKDF_SHA512 KEM = 0x12 + // KEM_X25519_HKDF_SHA256 is a KEM using X25519 Diffie-Hellman function + // and HKDF with SHA-256. + KEM_X25519_HKDF_SHA256 KEM = 0x20 +) + +func (k KEM) Scheme() kem.Scheme { + switch k { + case KEM_P256_HKDF_SHA256: + return kem.DHP256HKDFSHA256() + case KEM_P384_HKDF_SHA384: + return kem.DHP384HKDFSHA384() + case KEM_P521_HKDF_SHA512: + return kem.DHP521HKDFSHA512() + case KEM_X25519_HKDF_SHA256: + return kem.DHX25519HKDFSHA256() + default: + panic("invalid kem suite") + } +} + +func (k KEM) IsValid() bool { + switch k { + case KEM_P256_HKDF_SHA256, KEM_P384_HKDF_SHA384, KEM_P521_HKDF_SHA512, + KEM_X25519_HKDF_SHA256: + return true + default: + return false + } +} + +// ----------------------------------------------------------------------------- + +type KDF uint16 + +//nolint:stylecheck +const ( + // KDF_HKDF_SHA256 is a KDF using HKDF with SHA-256. + KDF_HKDF_SHA256 KDF = 0x01 + // KDF_HKDF_SHA384 is a KDF using HKDF with SHA-384. + KDF_HKDF_SHA384 KDF = 0x02 + // KDF_HKDF_SHA512 is a KDF using HKDF with SHA-512. + KDF_HKDF_SHA512 KDF = 0x03 +) + +func (k KDF) IsValid() bool { + switch k { + case KDF_HKDF_SHA256, KDF_HKDF_SHA384, KDF_HKDF_SHA512: + return true + default: + return false + } +} + +func (k KDF) ExtractSize() uint16 { + switch k { + case KDF_HKDF_SHA256: + return uint16(crypto.SHA256.Size()) + case KDF_HKDF_SHA384: + return uint16(crypto.SHA384.Size()) + case KDF_HKDF_SHA512: + return uint16(crypto.SHA512.Size()) + default: + panic("invalid hash") + } +} + +func (k KDF) Extract(secret, salt []byte) []byte { + return hkdf.Extract(k.hash(), secret, salt) +} + +func (k KDF) Expand(prk, labeledInfo []byte, outputLen uint16) ([]byte, error) { + extractSize := k.ExtractSize() + // https://www.rfc-editor.org/rfc/rfc9180.html#kdf-input-length + if len(prk) < int(extractSize) { + return nil, fmt.Errorf("pseudorandom key must be at least %d bytes", extractSize) + } + // https://www.rfc-editor.org/rfc/rfc9180.html#name-secret-export + if maxLength := 255 * extractSize; outputLen > maxLength { + return nil, fmt.Errorf("expansion length is limited to %d", maxLength) + } + + r := hkdf.Expand(k.hash(), prk, labeledInfo) + out := make([]byte, outputLen) + if _, err := io.ReadFull(r, out); err != nil { + return nil, fmt.Errorf("unable to generate value from kdf: %w", err) + } + + return out, nil +} + +func (k KDF) hash() func() hash.Hash { + switch k { + case KDF_HKDF_SHA256: + return sha256.New + case KDF_HKDF_SHA384: + return sha512.New384 + case KDF_HKDF_SHA512: + return sha512.New + default: + panic("invalid hash") + } +} + +// ----------------------------------------------------------------------------- + +type AEAD uint16 + +//nolint:stylecheck +const ( + // AEAD_AES128GCM is AES-128 block cipher in Galois Counter Mode (GCM). + AEAD_AES128GCM AEAD = 0x01 + // AEAD_AES256GCM is AES-256 block cipher in Galois Counter Mode (GCM). + AEAD_AES256GCM AEAD = 0x02 + // AEAD_ChaCha20Poly1305 is ChaCha20 stream cipher and Poly1305 MAC. + AEAD_ChaCha20Poly1305 AEAD = 0x03 + // AEAD_EXPORT_ONLY is reserved for applications that only use the Exporter + // interface. + AEAD_EXPORT_ONLY AEAD = 0xFFFF +) + +func (a AEAD) IsValid() bool { + switch a { + case AEAD_AES128GCM, AEAD_AES256GCM, AEAD_ChaCha20Poly1305, AEAD_EXPORT_ONLY: + return true + default: + return false + } +} + +func (a AEAD) New(key []byte) (cipher.AEAD, error) { + switch a { + case AEAD_AES128GCM, AEAD_AES256GCM: + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + return cipher.NewGCM(block) + case AEAD_ChaCha20Poly1305: + return chacha20poly1305.New(key) + case AEAD_EXPORT_ONLY: + return nil, errors.New("AEAD cipher can't be initialized in export-only mode") + default: + panic("invalid aead") + } +} + +func (a AEAD) KeySize() uint16 { + switch a { + case AEAD_AES128GCM: + return 16 + case AEAD_AES256GCM: + return 32 + case AEAD_ChaCha20Poly1305: + return chacha20poly1305.KeySize + case AEAD_EXPORT_ONLY: + return 0 + default: + panic("invalid aead") + } +} + +func (a AEAD) NonceSize() uint16 { + switch a { + case AEAD_AES128GCM, + AEAD_AES256GCM, + AEAD_ChaCha20Poly1305: + return 12 + case AEAD_EXPORT_ONLY: + return 0 + default: + panic("invalid aead") + } +} diff --git a/pkg/sdk/security/crypto/hpke/keyschedule.go b/pkg/sdk/security/crypto/hpke/keyschedule.go new file mode 100644 index 00000000..15d75167 --- /dev/null +++ b/pkg/sdk/security/crypto/hpke/keyschedule.go @@ -0,0 +1,190 @@ +// SPDX-FileCopyrightText: 2019-2023 Thibault NORMAND +// +// SPDX-License-Identifier: Apache-2.0 AND MIT + +package hpke + +import ( + "bytes" + "crypto/cipher" + "encoding/binary" + "errors" + "fmt" + "sync/atomic" + + "github.com/awnumar/memguard" +) + +var ( + defaultPSK = []byte("") + defaultPSKID = []byte("") +) + +// Exporter describes key derivation operation. +type Exporter interface { + Export(exporterContext []byte, length uint16) ([]byte, error) +} + +type context struct { + suite Suite + aead cipher.AEAD + sharedSecret []byte + keyScheduleCtx []byte + secret []byte + key []byte + baseNonce []byte + counter *atomic.Uint64 + exporterSecret []byte +} + +func (s Suite) verifyPSK(encMode mode, psk, pskID []byte) error { + gotPsk := !bytes.Equal(psk, defaultPSK) + gotPskID := !bytes.Equal(pskID, defaultPSKID) + + // Check arguments + switch { + case gotPsk && !gotPskID, !gotPsk && gotPskID: + return errors.New("inconsistent PSK inputs") + default: + } + + switch encMode { + case modeBase, modeAuth: + if gotPsk { + return errors.New("PSK input provided when not needed") + } + case modePsk, modeAuthPsk: + if !gotPsk { + return errors.New("missing required PSK input") + } + } + + return nil +} + +func (s Suite) keySchedule(encMode mode, sharedSecret, info, psk, pskID []byte) (*context, error) { + // https://www.rfc-editor.org/rfc/rfc9180.html#section-7.2.1-4 + switch { + case len(info) > 64: + return nil, fmt.Errorf("psk must not be larger than 64 bytes") + case len(psk) > 64: + return nil, fmt.Errorf("pskID must not be larger than 64 bytes") + case len(pskID) > 64: + return nil, fmt.Errorf("info must not be larger than 64 bytes") + } + + if err := s.verifyPSK(encMode, psk, pskID); err != nil { + return nil, err + } + + pskIDHash := s.labeledExtract([]byte(""), []byte("psk_id_hash"), pskID) + infoHash := s.labeledExtract([]byte(""), []byte("info_hash"), info) + + // key_schedule_context = concat(mode, psk_id_hash, info_hash) + keyScheduleContext := append([]byte{}, byte(encMode)) + keyScheduleContext = append(keyScheduleContext, pskIDHash...) + keyScheduleContext = append(keyScheduleContext, infoHash...) + + secret := s.labeledExtract(sharedSecret, []byte("secret"), psk) + + var ( + aead cipher.AEAD + key, baseNonce []byte + ) + if s.aeadID != AEAD_EXPORT_ONLY { + var err error + + key, err = s.labeledExpand(secret, []byte("key"), keyScheduleContext, s.aeadID.KeySize()) + if err != nil { + return nil, fmt.Errorf("unable to derive encryption key: %w", err) + } + aead, err = s.aeadID.New(key) + if err != nil { + return nil, fmt.Errorf("unable to initialize AEAD encryption: %w", err) + } + + baseNonce, err = s.labeledExpand(secret, []byte("base_nonce"), keyScheduleContext, s.aeadID.NonceSize()) + if err != nil { + return nil, fmt.Errorf("unable to derive base nonce: %w", err) + } + } + + exporterSecret, err := s.labeledExpand(secret, []byte("exp"), keyScheduleContext, s.kdfID.ExtractSize()) + if err != nil { + return nil, fmt.Errorf("unable to derive exporter secret: %w", err) + } + + return &context{ + suite: s, + aead: aead, + sharedSecret: sharedSecret, + keyScheduleCtx: keyScheduleContext, + secret: secret, + key: key, + baseNonce: baseNonce, + counter: &atomic.Uint64{}, + exporterSecret: exporterSecret, + }, nil +} + +func (c *context) Seal(plaintext, aad []byte) ([]byte, error) { + if c.suite.aeadID == AEAD_EXPORT_ONLY { + return nil, errors.New("seal operation not available in export only mode") + } + + ct := c.aead.Seal(nil, c.computeNonce(c.counter.Load()), plaintext, aad) + if err := c.incrementCounter(); err != nil { + memguard.WipeBytes(ct) + return nil, err + } + + return ct, nil +} + +func (c *context) Open(ciphertext, aad []byte) ([]byte, error) { + if c.suite.aeadID == AEAD_EXPORT_ONLY { + return nil, errors.New("open operation not available in export only mode") + } + + pt, err := c.aead.Open(nil, c.computeNonce(c.counter.Load()), ciphertext, aad) + if err != nil { + return nil, err + } + + if err := c.incrementCounter(); err != nil { + memguard.WipeBytes(pt) + return nil, err + } + + return pt, nil +} + +func (c *context) Export(exporterContext []byte, outputLen uint16) ([]byte, error) { + // https://www.rfc-editor.org/rfc/rfc9180.html#section-7.2.1-4 + if len(exporterContext) > 64 { + return nil, errors.New("exporter context must be less than 64 bytes") + } + return c.suite.labeledExpand(c.exporterSecret, []byte("sec"), exporterContext, outputLen) +} + +func (c *context) computeNonce(seq uint64) []byte { + buf := make([]byte, 8) + binary.BigEndian.PutUint64(buf, seq) + nonce := make([]byte, c.aead.NonceSize()) + copy(nonce, c.baseNonce) + for i := range buf { + // Apply XOR on last 8 bytes only. + nonce[c.aead.NonceSize()-8+i] ^= buf[i] + } + + return nonce +} + +func (c *context) incrementCounter() error { + if c.counter.Load() >= (1<<(8*c.aead.NonceSize()))-1 { + return errors.New("message limit reached") + } + c.counter.Add(1) + + return nil +} diff --git a/pkg/sdk/security/crypto/hpke/receiver.go b/pkg/sdk/security/crypto/hpke/receiver.go new file mode 100644 index 00000000..03381c64 --- /dev/null +++ b/pkg/sdk/security/crypto/hpke/receiver.go @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: 2019-2023 Thibault NORMAND +// +// SPDX-License-Identifier: Apache-2.0 AND MIT + +package hpke + +import ( + "crypto/ecdh" + "fmt" +) + +// Receiver describes message receiver contract. +type Receiver interface { + SetupBase(enc []byte) (Opener, error) + SetupPSK(enc []byte, psk, pskID []byte) (Opener, error) + SetupAuth(enc []byte, pkS *ecdh.PublicKey) (Opener, error) + SetupAuthPSK(enc []byte, psk, pskID []byte, pkS *ecdh.PublicKey) (Opener, error) +} + +// Opener decrypts a ciphertext using an AEAD encryption. +type Opener interface { + Exporter + + // Open tries to authenticate and decrypt a ciphertext with associated + // additional data. The nonce is handled internally. + Open(ct, aad []byte) (pt []byte, err error) +} + +type receiver struct { + Suite + skR *ecdh.PrivateKey + info []byte +} + +func (r *receiver) SetupBase(enc []byte) (Opener, error) { + // shared_secret, enc = Encap(pkR) + ss, err := r.kemID.Scheme().Decapsulate(enc, r.skR) + if err != nil { + return nil, fmt.Errorf("receiver: %w", err) + } + + ctx, err := r.keySchedule(modeBase, ss, r.info, defaultPSK, defaultPSKID) + if err != nil { + return nil, fmt.Errorf("receiver: unable to initialize key schedule: %w", err) + } + + return ctx, nil +} + +func (r *receiver) SetupPSK(enc []byte, psk, pskID []byte) (Opener, error) { + // shared_secret, enc = Encap(pkR) + ss, err := r.kemID.Scheme().Decapsulate(enc, r.skR) + if err != nil { + return nil, fmt.Errorf("receiver: %w", err) + } + + ctx, err := r.keySchedule(modePsk, ss, r.info, psk, pskID) + if err != nil { + return nil, fmt.Errorf("receiver: unable to initialize key schedule: %w", err) + } + + return ctx, nil +} + +func (r *receiver) SetupAuth(enc []byte, pkS *ecdh.PublicKey) (Opener, error) { + // shared_secret = AuthDecap(enc, skR, pkS) + ss, err := r.kemID.Scheme().AuthDecapsulate(enc, r.skR, pkS) + if err != nil { + return nil, fmt.Errorf("receiver: %w", err) + } + + ctx, err := r.keySchedule(modeAuth, ss, r.info, defaultPSK, defaultPSKID) + if err != nil { + return nil, fmt.Errorf("receiver: unable to initialize key schedule: %w", err) + } + + return ctx, nil +} + +func (r *receiver) SetupAuthPSK(enc []byte, psk, pskID []byte, pkS *ecdh.PublicKey) (Opener, error) { + // shared_secret = AuthDecap(enc, skR, pkS) + ss, err := r.kemID.Scheme().AuthDecapsulate(enc, r.skR, pkS) + if err != nil { + return nil, fmt.Errorf("receiver: %w", err) + } + + ctx, err := r.keySchedule(modeAuthPsk, ss, r.info, psk, pskID) + if err != nil { + return nil, fmt.Errorf("receiver: unable to initialize key schedule: %w", err) + } + + return ctx, nil +} diff --git a/pkg/sdk/security/crypto/hpke/sender.go b/pkg/sdk/security/crypto/hpke/sender.go new file mode 100644 index 00000000..9d6bd714 --- /dev/null +++ b/pkg/sdk/security/crypto/hpke/sender.go @@ -0,0 +1,135 @@ +// SPDX-FileCopyrightText: 2019-2023 Thibault NORMAND +// +// SPDX-License-Identifier: Apache-2.0 AND MIT + +package hpke + +import ( + "crypto/ecdh" + "crypto/rand" + "fmt" + "io" +) + +// Sender describes message sender contract. +type Sender interface { + SetupBase() ([]byte, Sealer, error) + SetupPSK(psk, pskID []byte) ([]byte, Sealer, error) + SetupAuth(skS *ecdh.PrivateKey) ([]byte, Sealer, error) + SetupAuthPSK(psk, pskID []byte, skS *ecdh.PrivateKey) ([]byte, Sealer, error) +} + +// Sealer encrypts a plaintext using an AEAD encryption. +type Sealer interface { + Exporter + + // Seal encrypts a given plaintext a plaintext with associated data. + // The nonce is managed internally. + Seal(pt, aad []byte) (ct []byte, err error) +} + +type sender struct { + Suite + pkR *ecdh.PublicKey + info []byte +} + +func (s *sender) SetupBase() ([]byte, Sealer, error) { + return s.setupBase(rand.Reader) +} + +func (s *sender) setupBase(r io.Reader) ([]byte, Sealer, error) { + // Generate a seed + seed := make([]byte, s.kemID.Scheme().PrivateKeySize()) + if _, err := io.ReadFull(r, seed); err != nil { + return nil, nil, fmt.Errorf("unable to generate encapsulation seed: %w", err) + } + + // shared_secret, enc = Encap(pkR) + ss, enc, err := s.kemID.Scheme().EncapsulateDeterministically(seed, s.pkR) + if err != nil { + return nil, nil, fmt.Errorf("sender: %w", err) + } + + ctx, err := s.keySchedule(modeBase, ss, s.info, defaultPSK, defaultPSKID) + if err != nil { + return nil, nil, fmt.Errorf("sender: unable to initialize key schedule: %w", err) + } + + return enc, ctx, nil +} + +func (s *sender) SetupPSK(psk, pskID []byte) ([]byte, Sealer, error) { + return s.setupPSK(rand.Reader, psk, pskID) +} + +func (s *sender) setupPSK(r io.Reader, psk, pskID []byte) ([]byte, Sealer, error) { + // Generate a seed + seed := make([]byte, s.kemID.Scheme().PrivateKeySize()) + if _, err := io.ReadFull(r, seed); err != nil { + return nil, nil, fmt.Errorf("unable to generate encapsulation seed: %w", err) + } + + // shared_secret, enc = Encap(pkR) + ss, enc, err := s.kemID.Scheme().EncapsulateDeterministically(seed, s.pkR) + if err != nil { + return nil, nil, fmt.Errorf("sender: %w", err) + } + + ctx, err := s.keySchedule(modePsk, ss, s.info, psk, pskID) + if err != nil { + return nil, nil, fmt.Errorf("sender: unable to initialize key schedule: %w", err) + } + + return enc, ctx, nil +} + +func (s *sender) SetupAuth(skS *ecdh.PrivateKey) ([]byte, Sealer, error) { + return s.setupAuth(rand.Reader, skS) +} + +func (s *sender) setupAuth(r io.Reader, skS *ecdh.PrivateKey) ([]byte, Sealer, error) { + // Generate a seed + seed := make([]byte, s.kemID.Scheme().PrivateKeySize()) + if _, err := io.ReadFull(r, seed); err != nil { + return nil, nil, fmt.Errorf("unable to generate encapsulation seed: %w", err) + } + + // shared_secret, enc = AuthEncap(pkR, skS) + ss, enc, err := s.kemID.Scheme().AuthEncapsulateDeterministically(seed, s.pkR, skS) + if err != nil { + return nil, nil, fmt.Errorf("sender: %w", err) + } + + ctx, err := s.keySchedule(modeAuth, ss, s.info, defaultPSK, defaultPSKID) + if err != nil { + return nil, nil, fmt.Errorf("sender: unable to initialize key schedule: %w", err) + } + + return enc, ctx, nil +} + +func (s *sender) SetupAuthPSK(psk, pskID []byte, skS *ecdh.PrivateKey) ([]byte, Sealer, error) { + return s.setupAuthPSK(rand.Reader, psk, pskID, skS) +} + +func (s *sender) setupAuthPSK(r io.Reader, psk, pskID []byte, skS *ecdh.PrivateKey) ([]byte, Sealer, error) { + // Generate a seed + seed := make([]byte, s.kemID.Scheme().PrivateKeySize()) + if _, err := io.ReadFull(r, seed); err != nil { + return nil, nil, fmt.Errorf("unable to generate encapsulation seed: %w", err) + } + + // shared_secret, enc = AuthEncap(pkR, skS) + ss, enc, err := s.kemID.Scheme().AuthEncapsulateDeterministically(seed, s.pkR, skS) + if err != nil { + return nil, nil, fmt.Errorf("sender: %w", err) + } + + ctx, err := s.keySchedule(modeAuthPsk, ss, s.info, psk, pskID) + if err != nil { + return nil, nil, fmt.Errorf("sender: unable to initialize key schedule: %w", err) + } + + return enc, ctx, nil +} diff --git a/pkg/sdk/security/crypto/hpke/suite.go b/pkg/sdk/security/crypto/hpke/suite.go new file mode 100644 index 00000000..e9d77d56 --- /dev/null +++ b/pkg/sdk/security/crypto/hpke/suite.go @@ -0,0 +1,88 @@ +// SPDX-FileCopyrightText: 2019-2023 Thibault NORMAND +// +// SPDX-License-Identifier: Apache-2.0 AND MIT + +package hpke + +import ( + "crypto/ecdh" + "encoding/binary" +) + +// New initializes a new HPKE suite. +func New(kemID KEM, kdfID KDF, aeadID AEAD) *Suite { + return &Suite{ + kemID: kemID, + kdfID: kdfID, + aeadID: aeadID, + } +} + +// Suite represents HPKE suite parameters. +type Suite struct { + kemID KEM + kdfID KDF + aeadID AEAD +} + +// IsValid checks if the suite is initialized with valid values. +func (s Suite) IsValid() bool { + return s.kemID.IsValid() && s.kdfID.IsValid() && s.aeadID.IsValid() +} + +// SuiteID returns the public suite identifier used for material derivation. +func (s Suite) suiteID() []byte { + var out [10]byte + // suite_id = concat("HPKE", I2OSP(kem_id, 2), ISOSP(kdf_id, 2), ISOSP(aead_id, 2)) + out[0], out[1], out[2], out[3] = 'H', 'P', 'K', 'E' + binary.BigEndian.PutUint16(out[4:6], uint16(s.kemID)) + binary.BigEndian.PutUint16(out[6:8], uint16(s.kdfID)) + binary.BigEndian.PutUint16(out[8:10], uint16(s.aeadID)) + return out[:] +} + +// Params returns suite parameters. +func (s Suite) Params() (KEM, KDF, AEAD) { + return s.kemID, s.kdfID, s.aeadID +} + +// Sender returns a message sender context builder. +func (s Suite) Sender(pkR *ecdh.PublicKey, info []byte) Sender { + return &sender{ + Suite: s, + pkR: pkR, + info: info, + } +} + +// Receiver returns a message receiver context builder. +func (s Suite) Receiver(skR *ecdh.PrivateKey, info []byte) Receiver { + return &receiver{ + Suite: s, + skR: skR, + info: info, + } +} + +// ----------------------------------------------------------------------------- + +func (s Suite) labeledExtract(salt, label, ikm []byte) []byte { + // labeled_ikm = concat("HPKE-v1", suite_id, label, ikm) + labeledIKM := append([]byte("HPKE-v1"), s.suiteID()...) + labeledIKM = append(labeledIKM, label...) + labeledIKM = append(labeledIKM, ikm...) + + return s.kdfID.Extract(labeledIKM, salt) +} + +func (s Suite) labeledExpand(prk, label, info []byte, outputLen uint16) ([]byte, error) { + labeledInfo := make([]byte, 2, 2+7+10+len(label)+len(info)) + // labeled_info = concat(I2OSP(L, 2), "HPKE-v1", suite_id, label, info) + binary.BigEndian.PutUint16(labeledInfo[0:2], outputLen) + labeledInfo = append(labeledInfo, []byte("HPKE-v1")...) + labeledInfo = append(labeledInfo, s.suiteID()...) + labeledInfo = append(labeledInfo, label...) + labeledInfo = append(labeledInfo, info...) + + return s.kdfID.Expand(prk, labeledInfo, outputLen) +} diff --git a/pkg/sdk/security/crypto/hpke/vector_test.go b/pkg/sdk/security/crypto/hpke/vector_test.go new file mode 100644 index 00000000..8871bd89 --- /dev/null +++ b/pkg/sdk/security/crypto/hpke/vector_test.go @@ -0,0 +1,250 @@ +// SPDX-FileCopyrightText: 2019-2023 Thibault NORMAND +// +// SPDX-License-Identifier: Apache-2.0 AND MIT + +package hpke + +import ( + "bytes" + "compress/gzip" + "crypto/ecdh" + "encoding/hex" + "encoding/json" + "fmt" + "os" + "testing" + + "github.com/stretchr/testify/require" + "zntr.io/harp/v2/pkg/sdk/ioutil" +) + +type hexByteSlice []byte + +//nolint:wrapcheck // No need to wrap the error +func (m *hexByteSlice) UnmarshalJSON(b []byte) error { + var data string + if err := json.Unmarshal(b, &data); err != nil { + return err + } + + // Decode hex + raw, err := hex.DecodeString(data) + *m = raw + return err +} + +type encryptionVector struct { + Aad hexByteSlice `json:"aad"` + Ciphertext hexByteSlice `json:"ct"` + Nonce hexByteSlice `json:"nonce"` + Plaintext hexByteSlice `json:"pt"` +} + +type exportVector struct { + ExportContext hexByteSlice `json:"exporter_context"` + ExportLength int `json:"L"` + ExportValue hexByteSlice `json:"exported_value"` +} + +type vector struct { + ModeID uint8 `json:"mode"` + KemID uint16 `json:"kem_id"` + KdfID uint16 `json:"kdf_id"` + AeadID uint16 `json:"aead_id"` + Info hexByteSlice `json:"info"` + Ier hexByteSlice `json:"ier,omitempty"` + IkmR hexByteSlice `json:"ikmR"` + IkmE hexByteSlice `json:"ikmE,omitempty"` + IkmS hexByteSlice `json:"ikmS,omitempty"` + SkRm hexByteSlice `json:"skRm"` + SkEm hexByteSlice `json:"skEm,omitempty"` + SkSm hexByteSlice `json:"skSm,omitempty"` + Psk hexByteSlice `json:"psk,omitempty"` + PskID hexByteSlice `json:"psk_id,omitempty"` + PkSm hexByteSlice `json:"pkSm,omitempty"` + PkRm hexByteSlice `json:"pkRm"` + PkEm hexByteSlice `json:"pkEm,omitempty"` + Enc hexByteSlice `json:"enc"` + SharedSecret hexByteSlice `json:"shared_secret"` + KeyScheduleContext hexByteSlice `json:"key_schedule_context"` + Secret hexByteSlice `json:"secret"` + Key hexByteSlice `json:"key"` + BaseNonce hexByteSlice `json:"base_nonce"` + ExporterSecret hexByteSlice `json:"exporter_secret"` + Encryptions []encryptionVector `json:"encryptions"` + Exports []exportVector `json:"exports"` +} + +func TestRFCVector(t *testing.T) { + t.Parallel() + + root := os.DirFS("./testdata") + + vectorFile, err := root.Open("test-vectors.json.gz") + require.NoError(t, err) + + gzr, err := gzip.NewReader(vectorFile) + require.NoError(t, err) + + // Decompress in memory (max 25MB) + var out bytes.Buffer + _, err = ioutil.LimitCopy(&out, gzr, 25<<20) + require.NoError(t, err) + + // Decode JSON objects + var vectors []vector + dec := json.NewDecoder(&out) + dec.DisallowUnknownFields() + require.NoError(t, dec.Decode(&vectors)) + + for i, vector := range vectors { + vector := vector + t.Run(fmt.Sprintf("#%d", i), func(t *testing.T) { + t.Parallel() + + s := New(KEM(vector.KemID), KDF(vector.KdfID), AEAD(vector.AeadID)) + if !s.IsValid() { + kem, kdf, aead := s.Params() + t.Skipf("Skipping test with invalid suite params (%x/%x/%x)", kem, kdf, aead) + } + + sender, receiver := buildSenderAndReceiver(t, &vector, s) + require.NotNil(t, sender) + require.NotNil(t, receiver) + + sealer, opener := protocolSetup(t, &vector, sender, receiver, s) + require.NotNil(t, sealer) + require.NotNil(t, opener) + + // Restore original type to access private properties. + csealer, _ := sealer.(*context) + copener, _ := opener.(*context) + + checkKeyschedule(t, &vector, s, csealer) + checkKeyschedule(t, &vector, s, copener) + checkEncryptions(t, &vector, csealer, copener) + checkExports(t, &vector, csealer) + checkExports(t, &vector, copener) + }) + } +} + +func checkExports(t *testing.T, v *vector, ctx *context) { + t.Helper() + + for _, ce := range v.Exports { + out, err := ctx.Export(ce.ExportContext, uint16(ce.ExportLength)) + require.NoError(t, err) + require.Equal(t, []byte(ce.ExportValue), out) + } +} + +func checkEncryptions(t *testing.T, v *vector, sealer *context, opener *context) { + t.Helper() + + for i, ve := range v.Encryptions { + require.Equal(t, []byte(ve.Nonce), sealer.computeNonce(uint64(i))) + require.Equal(t, []byte(ve.Nonce), opener.computeNonce(uint64(i))) + + ct, err := sealer.Seal(ve.Plaintext, ve.Aad) + require.NoError(t, err) + + pt, err := opener.Open(ve.Ciphertext, ve.Aad) + require.NoError(t, err) + + require.Equal(t, []byte(ve.Plaintext), pt) + require.Equal(t, []byte(ve.Ciphertext), ct) + } +} + +func checkKeyschedule(t *testing.T, v *vector, s *Suite, ctx *context) { + t.Helper() + + require.NotNil(t, ctx) + require.Equal(t, []byte(v.KeyScheduleContext), ctx.keyScheduleCtx) + require.Equal(t, []byte(v.SharedSecret), ctx.sharedSecret) + require.Equal(t, []byte(v.Secret), ctx.secret) + if s.aeadID != AEAD_EXPORT_ONLY { + require.Equal(t, []byte(v.Key), ctx.key) + require.Equal(t, []byte(v.BaseNonce), ctx.baseNonce) + } + require.Equal(t, []byte(v.ExporterSecret), ctx.exporterSecret) +} + +func buildSenderAndReceiver(t *testing.T, v *vector, s *Suite) (Sender, Receiver) { + t.Helper() + + scheme := s.kemID.Scheme() + // Decode materials + pkR, err := scheme.DeserializePublicKey(v.PkRm) + require.NoError(t, err) + + skR, err := scheme.DeserializePrivateKey(v.SkRm) + require.NoError(t, err) + + sender := s.Sender(pkR, v.Info) + receiver := s.Receiver(skR, v.Info) + + return sender, receiver +} + +func protocolSetup(t *testing.T, v *vector, snd Sender, rcv Receiver, s *Suite) (sealer Sealer, opener Opener) { + t.Helper() + + var ( + enc []byte + skS *ecdh.PrivateKey + pkS *ecdh.PublicKey + errS, errR, errSK, errPK error + ) + + // Downgrade the type to get access to private functions + sender := snd.(*sender) + seedReader := bytes.NewReader(v.IkmE) + + scheme := s.kemID.Scheme() + + switch v.ModeID { + case uint8(modeBase): + enc, sealer, errS = sender.setupBase(seedReader) + if errS == nil { + opener, errR = rcv.SetupBase(enc) + } + case uint8(modePsk): + enc, sealer, errS = sender.setupPSK(seedReader, v.Psk, v.PskID) + if errS == nil { + opener, errR = rcv.SetupPSK(enc, v.Psk, v.PskID) + } + case uint8(modeAuth): + skS, errSK = scheme.DeserializePrivateKey(v.SkSm) + if errSK == nil { + pkS, errPK = scheme.DeserializePublicKey(v.PkSm) + if errPK == nil { + enc, sealer, errS = sender.setupAuth(seedReader, skS) + if errS == nil { + opener, errR = rcv.SetupAuth(enc, pkS) + } + } + } + case uint8(modeAuthPsk): + skS, errSK = scheme.DeserializePrivateKey(v.SkSm) + if errSK == nil { + pkS, errPK = scheme.DeserializePublicKey(v.PkSm) + if errPK == nil { + enc, sealer, errS = sender.setupAuthPSK(seedReader, v.Psk, v.PskID, skS) + if errS == nil { + opener, errR = rcv.SetupAuthPSK(enc, v.Psk, v.PskID, pkS) + } + } + } + default: + t.Errorf("unsupported mode %x", v.ModeID) + } + + require.NoError(t, errS) + require.NoError(t, errR) + require.NoError(t, errSK) + require.NoError(t, errPK) + + return sealer, opener +} diff --git a/pkg/sdk/security/crypto/kem/api.go b/pkg/sdk/security/crypto/kem/api.go new file mode 100644 index 00000000..cf0be1fa --- /dev/null +++ b/pkg/sdk/security/crypto/kem/api.go @@ -0,0 +1,92 @@ +// SPDX-FileCopyrightText: 2019-2023 Thibault NORMAND +// +// SPDX-License-Identifier: Apache-2.0 AND MIT + +package kem + +import ( + "crypto/ecdh" + "crypto/sha256" + "crypto/sha512" +) + +// Scheme defines the default KEM suite contract. +type Scheme interface { + SuiteID() []byte + GenerateKeyPair() (*ecdh.PublicKey, *ecdh.PrivateKey, error) + DeriveKeyPair(seed []byte) (*ecdh.PublicKey, *ecdh.PrivateKey, error) + SerializePublicKey(pkX *ecdh.PublicKey) []byte + DeserializePublicKey(pkXxm []byte) (*ecdh.PublicKey, error) + SerializePrivateKey(sk *ecdh.PrivateKey) []byte + DeserializePrivateKey(skRaw []byte) (*ecdh.PrivateKey, error) + Encapsulate(pkR *ecdh.PublicKey) (ss, enc []byte, err error) + EncapsulateDeterministically(seed []byte, pkR *ecdh.PublicKey) (ss, enc []byte, err error) + Decapsulate(enc []byte, skR *ecdh.PrivateKey) ([]byte, error) + AuthEncapsulate(pkR *ecdh.PublicKey, skS *ecdh.PrivateKey) (ss, enc []byte, err error) + AuthEncapsulateDeterministically(seed []byte, pkR *ecdh.PublicKey, skS *ecdh.PrivateKey) (ss, enc []byte, err error) + AuthDecapsulate(enc []byte, skR *ecdh.PrivateKey, pkS *ecdh.PublicKey) ([]byte, error) + EncapsulationSize() uint16 + PublicKeySize() uint16 + PrivateKeySize() uint16 + SecretSize() uint16 +} + +// DHP256HKDFSHA256 defines a KEM Suite based on P-256 curve with HKDF-SHA256 +// for shared secret derivation. +func DHP256HKDFSHA256() Scheme { + return &dhkem{ + kemID: 16, + curve: ecdh.P256(), + fh: sha256.New, + nSecret: 32, + nEnc: 65, + nPk: 65, + nSk: 32, + keyDeriverFunc: ecDeriver(ecdh.P256()), + } +} + +// DHP384HKDFSHA384 defines a KEM Suite based on P-384 curve with HKDF-SHA384 +// for shared secret derivation. +func DHP384HKDFSHA384() Scheme { + return &dhkem{ + kemID: 17, + curve: ecdh.P384(), + fh: sha512.New384, + nSecret: 48, + nEnc: 97, + nPk: 97, + nSk: 48, + keyDeriverFunc: ecDeriver(ecdh.P384()), + } +} + +// DHP521HKDFSHA512 defines a KEM Suite based on P-521 curve with HKDF-SHA512 +// for shared secret derivation. +func DHP521HKDFSHA512() Scheme { + return &dhkem{ + kemID: 18, + curve: ecdh.P521(), + fh: sha512.New, + nSecret: 64, + nEnc: 133, + nPk: 133, + nSk: 66, + keyDeriverFunc: ecDeriver(ecdh.P521()), + } +} + +// DHX25519HKDFSHA256 defines a KEM Suite based on Curve25519 curve with +// HKDF-SHA256 for shared secret derivation. +func DHX25519HKDFSHA256() Scheme { + return &dhkem{ + kemID: 32, + curve: ecdh.X25519(), + fh: sha256.New, + nSecret: 32, + nEnc: 32, + nPk: 32, + nSk: 32, + keyDeriverFunc: xDeriver, + } +} diff --git a/pkg/sdk/security/crypto/kem/dhkem.go b/pkg/sdk/security/crypto/kem/dhkem.go new file mode 100644 index 00000000..eef8ba8b --- /dev/null +++ b/pkg/sdk/security/crypto/kem/dhkem.go @@ -0,0 +1,376 @@ +// SPDX-FileCopyrightText: 2019-2023 Thibault NORMAND +// +// SPDX-License-Identifier: Apache-2.0 AND MIT + +package kem + +import ( + "crypto/ecdh" + "crypto/rand" + "encoding/binary" + "errors" + "fmt" + "hash" + "io" + + "github.com/awnumar/memguard" + "golang.org/x/crypto/hkdf" +) + +var ( + // ErrDeserialization is raised when the given material can't be decoded as + // the expected key type. + ErrDeserialization = errors.New("unable to deserialize key content") + // ErrEncap is raised when an error occurred during shared secret encapsulation. + ErrEncap = errors.New("unable to encapsulate the shared secret") + // ErrDecap is raised when an error occurred during shared secret decapsulation. + ErrDecap = errors.New("unable to decapsulate the shared secret") +) + +// Implements https://www.rfc-editor.org/rfc/rfc9180.html#name-dh-based-kem-dhkem +type dhkem struct { + kemID uint16 + curve ecdh.Curve + fh func() hash.Hash + nSecret uint16 + nEnc uint16 + nPk uint16 + nSk uint16 + keyDeriverFunc keyDeriver +} + +// SuiteID returns the public suite identifier used for material derivation. +func (kem *dhkem) SuiteID() []byte { + var out [5]byte + // suite_id = concat("KEM", I2OSP(kem_id, 2)) + out[0], out[1], out[2] = 'K', 'E', 'M' + binary.BigEndian.PutUint16(out[3:5], kem.kemID) + return out[:] +} + +// PublicKeySize returns the serialized public key size. +func (kem *dhkem) PublicKeySize() uint16 { + return kem.nPk +} + +// PrivateKeySize returns the serialized private key size. +func (kem *dhkem) PrivateKeySize() uint16 { + return kem.nSk +} + +// EncapsulationSize returns the encapsulation size. +func (kem *dhkem) EncapsulationSize() uint16 { + return kem.nEnc +} + +// SecretSize returns the shared secret size. +func (kem *dhkem) SecretSize() uint16 { + return kem.nSecret +} + +// DeriveKeyPair generates deterministically according to the seed content a +// keypair. +func (kem *dhkem) DeriveKeyPair(seed []byte) (*ecdh.PublicKey, *ecdh.PrivateKey, error) { + return kem.keyDeriverFunc(kem, seed) +} + +// GenerateKeyPair generates a key associated to the suite. +func (kem *dhkem) GenerateKeyPair() (*ecdh.PublicKey, *ecdh.PrivateKey, error) { + sk, err := kem.curve.GenerateKey(rand.Reader) + if err != nil { + return nil, nil, fmt.Errorf("unable to generate key pair from the suite: %w", err) + } + + return sk.PublicKey(), sk, nil +} + +// SerializePublicKey exports the given public key as a byte array. +func (kem *dhkem) SerializePublicKey(pkX *ecdh.PublicKey) []byte { + raw := pkX.Bytes() + if len(raw) != int(kem.nPk) { + panic("invalid public key size") + } + + return raw +} + +// DeserializePublicKey reads the given content and try to extract a public key +// matching the suite public key type. +func (kem *dhkem) DeserializePublicKey(pkXxm []byte) (*ecdh.PublicKey, error) { + if len(pkXxm) != int(kem.nPk) { + return nil, errors.New("public key data size is invalid") + } + + return kem.curve.NewPublicKey(pkXxm) +} + +// SerializePrivateKey exports the given private key as a byte array. +func (kem *dhkem) SerializePrivateKey(sk *ecdh.PrivateKey) []byte { + raw := sk.Bytes() + if len(raw) != int(kem.nSk) { + panic("invalid private key size") + } + + return raw +} + +// DeserializePrivateKey reads the given content and try to extract a private key +// matching the suite private key type. +func (kem *dhkem) DeserializePrivateKey(raw []byte) (*ecdh.PrivateKey, error) { + if len(raw) != int(kem.nSk) { + return nil, errors.New("private key data size is invalid") + } + + return kem.curve.NewPrivateKey(raw) +} + +// EncapsulateDeterministically computes the shared secret and exports a deterministic +// encapsulated public key based on a remote static public key and the given seed. +// +// If you don't which encapsulation you should choose, consider using `Encapsulate` +// function. +func (kem *dhkem) EncapsulateDeterministically(seed []byte, pkR *ecdh.PublicKey) (ss, enc []byte, err error) { + if len(seed) != int(kem.nSk) { + return nil, nil, fmt.Errorf("seed is too short, got %d, expected %d", len(seed), kem.nSk) + } + + // skE, pkE = DeriveKeyPair() + pkE, skE, err := kem.DeriveKeyPair(seed) + if err != nil { + return nil, nil, fmt.Errorf("unable to generate ephemeral keypair: %v: %w", err, ErrEncap) + } + + return kem.encapsulate(pkE, skE, pkR) +} + +// Encapsulate computes the shared secret and exports encapsulated public key +// based on a remote static public key. +func (kem *dhkem) Encapsulate(pkR *ecdh.PublicKey) (ss, enc []byte, err error) { + // skE, pkE = GenerateKeyPair() + pkE, skE, err := kem.GenerateKeyPair() + if err != nil { + return nil, nil, fmt.Errorf("unable to generate ephemeral keypair: %v: %w", err, ErrEncap) + } + + return kem.encapsulate(pkE, skE, pkR) +} + +func (kem *dhkem) encapsulate(pkE *ecdh.PublicKey, skE *ecdh.PrivateKey, pkR *ecdh.PublicKey) (ss, enc []byte, err error) { + // dh = DH(skE, pkR) + dh, err := skE.ECDH(pkR) + if err != nil { + return nil, nil, fmt.Errorf("unable to compute key agreement: %v: %w", err, ErrEncap) + } + defer memguard.WipeBytes(dh) + + enc = kem.SerializePublicKey(pkE) + if len(enc) != int(kem.nEnc) { + return nil, nil, errors.New("invalid encapsulation size") + } + pkRm := kem.SerializePublicKey(pkR) + + // kem_context = concat(enc, pkRm) + kemContext := append([]byte{}, enc...) + kemContext = append(kemContext, pkRm...) + ssRaw, err := kem.extractAndExpand(dh, kemContext) + if err != nil { + return nil, nil, fmt.Errorf("unable to compute shared secret: %v: %w", err, ErrEncap) + } + + return ssRaw, enc, nil +} + +// Decapsulate computes the shared secret from the given encapsulated public key +// and a receiver static public key. +func (kem *dhkem) Decapsulate(enc []byte, skR *ecdh.PrivateKey) ([]byte, error) { + if len(enc) != int(kem.nEnc) { + return nil, fmt.Errorf("invalid encapsulation size: %w", ErrDecap) + } + + // Copy encapsulated data + localEnc := make([]byte, kem.nEnc) + copy(localEnc, enc) + + // Try to deserialize received public key. + pkE, err := kem.DeserializePublicKey(localEnc) + if err != nil { + return nil, fmt.Errorf("unable to deserialize public key: %v: %w", err, ErrDecap) + } + + // dh = DH(skR, pkE) + dh, err := skR.ECDH(pkE) + if err != nil { + return nil, fmt.Errorf("unable to compute key agreement: %v: %w", err, ErrDecap) + } + defer memguard.WipeBytes(dh) + + pkRm := kem.SerializePublicKey(skR.PublicKey()) + + // kem_context = concat(enc, pkRm) + kemContext := append([]byte{}, localEnc...) + kemContext = append(kemContext, pkRm...) + + // shared_secret = ExtractAndExpand(dh, kem_context) + ssRaw, err := kem.extractAndExpand(dh, kemContext) + if err != nil { + return nil, fmt.Errorf("unable to compute shared secret: %v: %w", err, ErrDecap) + } + + return ssRaw, nil +} + +// AuthEncapsulateDeterministically computes a shared secret, and an deterministic +// encapsulated public key based on mutual sender and receiver static keys authentication +// and the given seed. +// +// If you don't which encapsulation you should choose, consider using `AuthEncapsulate` +// function. +func (kem *dhkem) AuthEncapsulateDeterministically(seed []byte, pkR *ecdh.PublicKey, skS *ecdh.PrivateKey) (ss, enc []byte, err error) { + if len(seed) != int(kem.nSk) { + return nil, nil, fmt.Errorf("seed is too short, got %d, expected %d", len(seed), kem.nSk) + } + + // skE, pkE = DeriveKeyPair() + pkE, skE, err := kem.DeriveKeyPair(seed) + if err != nil { + return nil, nil, fmt.Errorf("unable to generate ephemeral keypair: %v: %w", err, ErrEncap) + } + + return kem.authEncapsulate(pkE, skE, pkR, skS) +} + +// Encapsulate computes the shared secret and exports encapsulated public key +// based on a remote static public key. +func (kem *dhkem) AuthEncapsulate(pkR *ecdh.PublicKey, skS *ecdh.PrivateKey) (ss, enc []byte, err error) { + // skE, pkE = GenerateKeyPair() + pkE, skE, err := kem.GenerateKeyPair() + if err != nil { + return nil, nil, fmt.Errorf("unable to generate ephemeral keypair: %v: %w", err, ErrEncap) + } + + return kem.authEncapsulate(pkE, skE, pkR, skS) +} + +// AuthEncapsulate computes a shared secret, and an encapsulated public key +// based on mutual sender and receiver static keys authentication. +func (kem *dhkem) authEncapsulate(pkE *ecdh.PublicKey, skE *ecdh.PrivateKey, pkR *ecdh.PublicKey, skS *ecdh.PrivateKey) (ss, enc []byte, err error) { + Ze, err := skE.ECDH(pkR) + if err != nil { + return nil, nil, fmt.Errorf("unable to copute ephemeral key agreement: %w", err) + } + defer memguard.WipeBytes(Ze) + + Zs, err := skS.ECDH(pkR) + if err != nil { + return nil, nil, fmt.Errorf("unable to compute static key agreement: %w", err) + } + defer memguard.WipeBytes(Zs) + + // dh = concat(DH(skE, pkR), DH(skS, pkR)) + dh := append([]byte{}, Ze...) + dh = append(dh, Zs...) + defer memguard.WipeBytes(dh) + + enc = kem.SerializePublicKey(pkE) + pkRm := kem.SerializePublicKey(pkR) + pkSm := kem.SerializePublicKey(skS.PublicKey()) + + // kem_context = concat(enc, pkRm) + kemContext := append([]byte{}, enc...) + kemContext = append(kemContext, pkRm...) + kemContext = append(kemContext, pkSm...) + + // shared_secret = ExtractAndExpand(dh, kem_context) + ssRaw, err := kem.extractAndExpand(dh, kemContext) + if err != nil { + return nil, nil, fmt.Errorf("unable to compute shared secret: %w", err) + } + + return ssRaw, enc, nil +} + +// AuthDecapsulate computes a shared secret from a received encapsulated public +// key based on mutual sender and receiver static keys authentication. +func (kem *dhkem) AuthDecapsulate(enc []byte, skR *ecdh.PrivateKey, pkS *ecdh.PublicKey) ([]byte, error) { + if len(enc) != int(kem.nEnc) { + return nil, errors.New("invalid encapsulation size") + } + + // Copy encapsulated data + localEnc := make([]byte, kem.nEnc) + copy(localEnc, enc) + + // Try to deserialize received public key. + pkE, err := kem.DeserializePublicKey(localEnc) + if err != nil { + return nil, fmt.Errorf("unable to deserialize public key: %w", err) + } + + Ze, err := skR.ECDH(pkE) + if err != nil { + return nil, fmt.Errorf("unable to compute ephemeral key agreement: %w", err) + } + defer memguard.WipeBytes(Ze) + + Zs, err := skR.ECDH(pkS) + if err != nil { + return nil, fmt.Errorf("unable to compute static key agreement: %w", err) + } + defer memguard.WipeBytes(Zs) + + // dh = concat(DH(skR, pkE), DH(skR, pkS)) + dh := append([]byte{}, Ze...) + dh = append(dh, Zs...) + defer memguard.WipeBytes(dh) + + enc = kem.SerializePublicKey(pkE) + pkRm := kem.SerializePublicKey(skR.PublicKey()) + pkSm := kem.SerializePublicKey(pkS) + + // kem_context = concat(enc, pkRm, pkSm) + kemContext := append([]byte{}, enc...) + kemContext = append(kemContext, pkRm...) + kemContext = append(kemContext, pkSm...) + + // shared_secret = ExtractAndExpand(dh, kem_context) + ssRaw, err := kem.extractAndExpand(dh, kemContext) + if err != nil { + return nil, fmt.Errorf("unable to compute shared secret: %w", err) + } + + return ssRaw, nil +} + +// ----------------------------------------------------------------------------- + +func (kem *dhkem) extractAndExpand(dh, kemContext []byte) ([]byte, error) { + eaePrk := kem.labeledExtract([]byte(""), []byte("eae_prk"), dh) + return kem.labeledExpand(eaePrk, []byte("shared_secret"), kemContext, kem.nSecret) +} + +func (kem *dhkem) labeledExtract(salt, label, ikm []byte) []byte { + // labeled_ikm = concat("HPKE-v1", suite_id, label, ikm) + labeledIKM := append([]byte("HPKE-v1"), kem.SuiteID()...) + labeledIKM = append(labeledIKM, label...) + labeledIKM = append(labeledIKM, ikm...) + + return hkdf.Extract(kem.fh, labeledIKM, salt) +} + +func (kem *dhkem) labeledExpand(prk, label, info []byte, outputLen uint16) ([]byte, error) { + labeledInfo := make([]byte, 2, 2+7+5+len(label)+len(info)) + // labeled_info = concat(I2OSP(L, 2), "HPKE-v1", suite_id, label, info) + binary.BigEndian.PutUint16(labeledInfo[0:2], outputLen) + labeledInfo = append(labeledInfo, []byte("HPKE-v1")...) + labeledInfo = append(labeledInfo, kem.SuiteID()...) + labeledInfo = append(labeledInfo, label...) + labeledInfo = append(labeledInfo, info...) + + r := hkdf.Expand(kem.fh, prk, labeledInfo) + out := make([]byte, outputLen) + if _, err := io.ReadFull(r, out); err != nil { + return nil, fmt.Errorf("unable to generate secret from prf: %w", err) + } + + return out, nil +} diff --git a/pkg/sdk/security/crypto/kem/dhkem_test.go b/pkg/sdk/security/crypto/kem/dhkem_test.go new file mode 100644 index 00000000..2e9d2231 --- /dev/null +++ b/pkg/sdk/security/crypto/kem/dhkem_test.go @@ -0,0 +1,69 @@ +// SPDX-FileCopyrightText: 2019-2023 Thibault NORMAND +// +// SPDX-License-Identifier: Apache-2.0 AND MIT + +package kem + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestEncapDecap(t *testing.T) { + t.Parallel() + + suites := []Scheme{ + DHP256HKDFSHA256(), + DHP384HKDFSHA384(), + DHP521HKDFSHA512(), + DHX25519HKDFSHA256(), + } + for _, suite := range suites { + suite := suite + t.Run("", func(t *testing.T) { + t.Parallel() + + // Generate long term keys + pk, sk, err := suite.GenerateKeyPair() + require.NoError(t, err) + + ss1, enc, err := suite.Encapsulate(pk) + require.NoError(t, err) + + ss2, err := suite.Decapsulate(enc, sk) + require.NoError(t, err) + require.Equal(t, ss1, ss2) + }) + } +} + +func TestAuthEncapAuthDecap(t *testing.T) { + t.Parallel() + + suites := []Scheme{ + DHP256HKDFSHA256(), + DHP384HKDFSHA384(), + DHP521HKDFSHA512(), + DHX25519HKDFSHA256(), + } + for _, suite := range suites { + suite := suite + t.Run("", func(t *testing.T) { + t.Parallel() + + // Generate long term keys + pkS, skS, err := suite.GenerateKeyPair() + require.NoError(t, err) + pkR, skR, err := suite.GenerateKeyPair() + require.NoError(t, err) + + ss1, enc, err := suite.AuthEncapsulate(pkR, skS) + require.NoError(t, err) + + ss2, err := suite.AuthDecapsulate(enc, skR, pkS) + require.NoError(t, err) + require.Equal(t, ss1, ss2) + }) + } +} diff --git a/pkg/sdk/security/crypto/kem/doc.go b/pkg/sdk/security/crypto/kem/doc.go new file mode 100644 index 00000000..95a405b4 --- /dev/null +++ b/pkg/sdk/security/crypto/kem/doc.go @@ -0,0 +1,7 @@ +// SPDX-FileCopyrightText: 2019-2023 Thibault NORMAND +// +// SPDX-License-Identifier: Apache-2.0 AND MIT + +// Package kem provides Key Encapsulation Mechanism used to derive a shared secret +// from asymmetric materials. +package kem diff --git a/pkg/sdk/security/crypto/kem/key_derivation.go b/pkg/sdk/security/crypto/kem/key_derivation.go new file mode 100644 index 00000000..450bd320 --- /dev/null +++ b/pkg/sdk/security/crypto/kem/key_derivation.go @@ -0,0 +1,70 @@ +// SPDX-FileCopyrightText: 2019-2023 Thibault NORMAND +// +// SPDX-License-Identifier: Apache-2.0 AND MIT + +package kem + +import ( + "crypto/ecdh" + "errors" + "fmt" +) + +type keyDeriver func(*dhkem, []byte) (*ecdh.PublicKey, *ecdh.PrivateKey, error) + +func ecDeriver(curve ecdh.Curve) keyDeriver { + return func(kem *dhkem, seed []byte) (*ecdh.PublicKey, *ecdh.PrivateKey, error) { + if len(seed) != int(kem.nSk) { + return nil, nil, errors.New("invalid seed size") + } + + dkpPrk := kem.labeledExtract([]byte(""), []byte("dkp_prk"), seed) + counter := 0 + + bitMask := byte(0xFF) + if curve == ecdh.P521() { + bitMask = byte(0x01) + } + + var sk *ecdh.PrivateKey + for { + if counter > 255 { + return nil, nil, errors.New("unable to derive keypair from seed") + } + + bytes, err := kem.labeledExpand(dkpPrk, []byte("candidate"), []byte{uint8(counter)}, kem.nSk) + if err != nil { + return nil, nil, fmt.Errorf("unable to expand seed prk: %w", err) + } + bytes[0] &= bitMask + + sk, err = kem.DeserializePrivateKey(bytes) + if err == nil { + break + } + + counter++ + } + + return sk.PublicKey(), sk, nil + } +} + +func xDeriver(kem *dhkem, seed []byte) (*ecdh.PublicKey, *ecdh.PrivateKey, error) { + if len(seed) != int(kem.nSk) { + return nil, nil, errors.New("invalid seed size") + } + + dkpPrk := kem.labeledExtract([]byte(""), []byte("dkp_prk"), seed) + skRaw, err := kem.labeledExpand(dkpPrk, []byte("sk"), []byte(""), kem.nSk) + if err != nil { + return nil, nil, fmt.Errorf("unable to generate secret key seed: %w", err) + } + + sk, err := ecdh.X25519().NewPrivateKey(skRaw) + if err != nil { + return nil, nil, fmt.Errorf("invalid secret key: %w", err) + } + + return sk.PublicKey(), sk, nil +} diff --git a/pkg/sdk/security/crypto/kem/key_derivation_test.go b/pkg/sdk/security/crypto/kem/key_derivation_test.go new file mode 100644 index 00000000..26132f55 --- /dev/null +++ b/pkg/sdk/security/crypto/kem/key_derivation_test.go @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: 2019-2023 Thibault NORMAND +// +// SPDX-License-Identifier: Apache-2.0 AND MIT + +package kem + +import ( + "crypto/ecdh" + "encoding/hex" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestXDeriver(t *testing.T) { + scheme := DHX25519HKDFSHA256() + + ikmE, _ := hex.DecodeString("7268600d403fce431561aef583ee1613527cff655c1343f29812e66706df3234") + skEm, _ := hex.DecodeString("52c4a758a802cd8b936eceea314432798d5baf2d7e9235dc084ab1b9cfa2f736") + pkEm, _ := hex.DecodeString("37fda3567bdbd628e88668c3c8d7e97d1d1253b6d4ea6d44c150f741f1bf4431") + + pk, sk, err := xDeriver(scheme.(*dhkem), ikmE) + require.NoError(t, err) + require.Equal(t, pk.Bytes(), pkEm) + require.Equal(t, sk.Bytes(), skEm) +} + +func TestECDeriver(t *testing.T) { + t.Run("P-256", func(t *testing.T) { + scheme := DHP256HKDFSHA256() + + ikmE, _ := hex.DecodeString("798d82a8d9ea19dbc7f2c6dfa54e8a6706f7cdc119db0813dacf8440ab37c857") + skEm, _ := hex.DecodeString("6b8de0873aed0c1b2d09b8c7ed54cbf24fdf1dfc7a47fa501f918810642d7b91") + pkEm, _ := hex.DecodeString("042224f3ea800f7ec55c03f29fc9865f6ee27004f818fcbdc6dc68932c1e52e15b79e264a98f2c535ef06745f3d308624414153b22c7332bc1e691cb4af4d53454") + + pk, sk, err := ecDeriver(ecdh.P256())(scheme.(*dhkem), ikmE) + require.NoError(t, err) + require.Equal(t, pk.Bytes(), pkEm) + require.Equal(t, sk.Bytes(), skEm) + }) + + // P-384 not present in vector tests. + + t.Run("P-521", func(t *testing.T) { + scheme := DHP521HKDFSHA512() + + ikmE, _ := hex.DecodeString("2270197b9f64f86e0eecd49076d05f8fb9f5272c0e7ea519182ae76417b69e7a16f4b0e44116023857b509b84c8a7e48686940cb3ff7e1266ab7c0f3a7ff7770f21b") + skEm, _ := hex.DecodeString("01e1b006811a044a56ce62427cd2ea34b19ef6990c510f6e08ed5e1056c2ac39f61687134d292ae559fd070e31428ab2873b798908c3579e7a6f57e2e26d0dc532e7") + pkEm, _ := hex.DecodeString("0401a514f452f316bda875c37ca40dd2ee5d93be7c80a81c423fb1500974d87314ffbe8d5aefd34e69d44f310cdf752519cad0a2ef1a240d67049e57222291aaffbb85004680e6232e8555c97eba731c7e0a47a1063e039d4c9e915da35f53ce5310ebdc0a9586b222ebad01ed9bbfb844c3fab4e49c06de034ef780bfc74b774cfabe93ac") + + pk, sk, err := ecDeriver(ecdh.P521())(scheme.(*dhkem), ikmE) + require.NoError(t, err) + require.Equal(t, pk.Bytes(), pkEm) + require.Equal(t, sk.Bytes(), skEm) + }) +} diff --git a/pkg/sdk/security/crypto/paseto/v4/helpers.go b/pkg/sdk/security/crypto/paseto/v4/helpers.go deleted file mode 100644 index 911b8318..00000000 --- a/pkg/sdk/security/crypto/paseto/v4/helpers.go +++ /dev/null @@ -1,362 +0,0 @@ -// SPDX-FileCopyrightText: 2019 Elasticsearch B.V. -// SPDX-FileCopyrightText: 2019-2023 Thibault NORMAND -// -// SPDX-License-Identifier: Apache-2.0 AND MIT - -package v4 - -import ( - "bytes" - "crypto/ed25519" - "encoding/base64" - "encoding/binary" - "errors" - "fmt" - "io" - - "zntr.io/harp/v2/pkg/sdk/security" - - "golang.org/x/crypto/blake2b" - "golang.org/x/crypto/chacha20" -) - -const ( - // KeyLength is the requested encryption key size. - KeyLength = 32 - nonceLength = 32 - macLength = 32 - encryptionKDFLength = 56 - authenticationKeyLength = 32 - v4LocalPrefix = "v4.local." - v4PublicPrefix = "v4.public." -) - -// PASETO v4 symmetric encryption primitive. -// https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Version4.md#encrypt -func Encrypt(r io.Reader, key, m []byte, f, i string) ([]byte, error) { - // Create random seed - var n [nonceLength]byte - if _, err := io.ReadFull(r, n[:]); err != nil { - return nil, fmt.Errorf("paseto: unable to generate random seed: %w", err) - } - - // Delegate to primitive - return encrypt(key, n[:], m, f, i) -} - -// PASETO v4 symmetric decryption primitive -// https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Version4.md#decrypt -func Decrypt(key, input []byte, f, i string) ([]byte, error) { - // Check arguments - if key == nil { - return nil, errors.New("paseto: key is nil") - } - if len(key) != KeyLength { - return nil, fmt.Errorf("paseto: invalid key length, it must be %d bytes long", KeyLength) - } - if input == nil { - return nil, errors.New("paseto: input is nil") - } - - // Check token header - if !bytes.HasPrefix(input, []byte(v4LocalPrefix)) { - return nil, errors.New("paseto: invalid token") - } - - // Trim prefix - input = input[len(v4LocalPrefix):] - - // Check footer usage - if f != "" { - // Split the footer and the body - parts := bytes.SplitN(input, []byte("."), 2) - if len(parts) != 2 { - return nil, errors.New("paseto: invalid token, footer is missing but expected") - } - - // Decode footer - footer := make([]byte, base64.RawURLEncoding.DecodedLen(len(parts[1]))) - if _, err := base64.RawURLEncoding.Decode(footer, parts[1]); err != nil { - return nil, fmt.Errorf("paseto: invalid token, footer has invalid encoding: %w", err) - } - - // Compare footer - if !security.SecureCompare([]byte(f), footer) { - return nil, errors.New("paseto: invalid token, footer mismatch") - } - - // Continue without footer - input = parts[0] - } - - // Decode token - raw := make([]byte, base64.RawURLEncoding.DecodedLen(len(input))) - if _, err := base64.RawURLEncoding.Decode(raw, input); err != nil { - return nil, fmt.Errorf("paseto: invalid token body: %w", err) - } - - // Extract components - n := raw[:nonceLength] - t := raw[len(raw)-macLength:] - c := raw[macLength : len(raw)-macLength] - - // Derive keys from seed and secret key - ek, n2, ak, err := kdf(key, n) - if err != nil { - return nil, fmt.Errorf("paseto: unable to derive keys from seed: %w", err) - } - - // Compute MAC - t2, err := mac(ak, v4LocalPrefix, n, c, f, i) - if err != nil { - return nil, fmt.Errorf("paseto: unable to compute MAC: %w", err) - } - - // Time-constant compare MAC - if !security.SecureCompare(t, t2) { - return nil, errors.New("paseto: invalid pre-authentication header") - } - - // Prepare XChaCha20 stream cipher - ciph, err := chacha20.NewUnauthenticatedCipher(ek, n2) - if err != nil { - return nil, fmt.Errorf("paseto: unable to initialize XChaCha20 cipher: %w", err) - } - - // Encrypt the payload - m := make([]byte, len(c)) - ciph.XORKeyStream(m, c) - - // No error - return m, nil -} - -// PASETO v4 public signature primitive. -// https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Version4.md#sign -func Sign(m []byte, sk ed25519.PrivateKey, f, i string) ([]byte, error) { - // Compute protected content - m2, err := pae([]byte(v4PublicPrefix), m, []byte(f), []byte(i)) - if err != nil { - return nil, fmt.Errorf("unable to prepare protected content: %w", err) - } - - // Sign protected content - sig := ed25519.Sign(sk, m2) - - // Prepare content - body := append([]byte{}, m...) - body = append(body, sig...) - - // Encode body as RawURLBase64 - encodedBody := make([]byte, base64.RawURLEncoding.EncodedLen(len(body))) - base64.RawURLEncoding.Encode(encodedBody, body) - - // Assemble final token - final := append([]byte(v4PublicPrefix), encodedBody...) - if f != "" { - // Encode footer as RawURLBase64 - encodedFooter := make([]byte, base64.RawURLEncoding.EncodedLen(len(f))) - base64.RawURLEncoding.Encode(encodedFooter, []byte(f)) - - // Assemble body and footer - final = append(final, append([]byte("."), encodedFooter...)...) - } - - // No error - return final, nil -} - -// PASETO v4 signature verification primitive. -// https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Version4.md#verify -func Verify(sm []byte, pk ed25519.PublicKey, f, i string) ([]byte, error) { - // Check token header - if !bytes.HasPrefix(sm, []byte(v4PublicPrefix)) { - return nil, errors.New("paseto: invalid token") - } - - // Trim prefix - sm = sm[len(v4PublicPrefix):] - - // Check footer usage - if f != "" { - // Split the footer and the body - parts := bytes.SplitN(sm, []byte("."), 2) - if len(parts) != 2 { - return nil, errors.New("paseto: invalid token, footer is missing but expected") - } - - // Decode footer - footer := make([]byte, base64.RawURLEncoding.DecodedLen(len(parts[1]))) - if _, err := base64.RawURLEncoding.Decode(footer, parts[1]); err != nil { - return nil, fmt.Errorf("paseto: invalid token, footer has invalid encoding: %w", err) - } - - // Compare footer - if !security.SecureCompare([]byte(f), footer) { - return nil, errors.New("paseto: invalid token, footer mismatch") - } - - // Continue without footer - sm = parts[0] - } - - // Decode token - raw := make([]byte, base64.RawURLEncoding.DecodedLen(len(sm))) - if _, err := base64.RawURLEncoding.Decode(raw, sm); err != nil { - return nil, fmt.Errorf("paseto: invalid token body: %w", err) - } - - // Extract components - m := raw[:len(raw)-ed25519.SignatureSize] - s := raw[len(raw)-ed25519.SignatureSize:] - - // Compute protected content - m2, err := pae([]byte(v4PublicPrefix), m, []byte(f), []byte(i)) - if err != nil { - return nil, fmt.Errorf("unable to prepare protected content: %w", err) - } - - // Check signature - if !ed25519.Verify(pk, m2, s) { - return nil, errors.New("paseto: invalid token signature") - } - - // No error - return m, nil -} - -// ----------------------------------------------------------------------------- - -func encrypt(key, n, m []byte, f, i string) ([]byte, error) { - // Check arguments - if len(key) != KeyLength { - return nil, fmt.Errorf("paseto: invalid key length, it must be %d bytes long", KeyLength) - } - if len(n) != nonceLength { - return nil, fmt.Errorf("paseto: invalid nonce length, it must be %d bytes long", nonceLength) - } - - // Derive keys from seed and secret key - ek, n2, ak, err := kdf(key, n) - if err != nil { - return nil, fmt.Errorf("paseto: unable to derive keys from seed: %w", err) - } - - // Prepare XChaCha20 stream cipher (nonce > 24bytes => XChacha) - ciph, err := chacha20.NewUnauthenticatedCipher(ek, n2) - if err != nil { - return nil, fmt.Errorf("paseto: unable to initialize XChaCha20 cipher: %w", err) - } - - // Encrypt the payload - c := make([]byte, len(m)) - ciph.XORKeyStream(c, m) - - // Compute MAC - t, err := mac(ak, v4LocalPrefix, n, c, f, i) - if err != nil { - return nil, fmt.Errorf("paseto: unable to compute MAC: %w", err) - } - - // Serialize final token - // h || base64url(n || c || t) - body := append([]byte{}, n...) - body = append(body, c...) - body = append(body, t...) - - // Encode body as RawURLBase64 - encodedBody := make([]byte, base64.RawURLEncoding.EncodedLen(len(body))) - base64.RawURLEncoding.Encode(encodedBody, body) - - // Assemble final token - final := append([]byte(v4LocalPrefix), encodedBody...) - if f != "" { - // Encode footer as RawURLBase64 - encodedFooter := make([]byte, base64.RawURLEncoding.EncodedLen(len(f))) - base64.RawURLEncoding.Encode(encodedFooter, []byte(f)) - - // Assemble body and footer - final = append(final, append([]byte("."), encodedFooter...)...) - } - - // No error - return final, nil -} - -func kdf(key, n []byte) (ek, n2, ak []byte, err error) { - // Derive encryption key - encKDF, err := blake2b.New(encryptionKDFLength, key) - if err != nil { - return nil, nil, nil, fmt.Errorf("unable to initialize encryption kdf: %w", err) - } - - // Domain separation (we use the same seed for 2 different purposes) - encKDF.Write([]byte("paseto-encryption-key")) - encKDF.Write(n) - tmp := encKDF.Sum(nil) - - // Split encryption key (Ek) and nonce (n2) - ek = tmp[:KeyLength] - n2 = tmp[KeyLength:] - - // Derive authentication key - authKDF, err := blake2b.New(authenticationKeyLength, key) - if err != nil { - return nil, nil, nil, fmt.Errorf("unable to initialize authentication kdf: %w", err) - } - - // Domain separation (we use the same seed for 2 different purposes) - authKDF.Write([]byte("paseto-auth-key-for-aead")) - authKDF.Write(n) - ak = authKDF.Sum(nil) - - // No error - return ek, n2, ak, nil -} - -func mac(ak []byte, h string, n, c []byte, f, i string) ([]byte, error) { - // Compute pre-authentication message - preAuth, err := pae([]byte(h), n, c, []byte(f), []byte(i)) - if err != nil { - return nil, fmt.Errorf("unable to compute pre-authentication content: %w", err) - } - - // Compute MAC - mac, err := blake2b.New(macLength, ak) - if err != nil { - return nil, fmt.Errorf("unable to in initialize MAC kdf: %w", err) - } - - // Hash pre-authentication content - mac.Write(preAuth) - - // No error - return mac.Sum(nil), nil -} - -// https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Common.md#authentication-padding -func pae(pieces ...[]byte) ([]byte, error) { - output := &bytes.Buffer{} - - // Encode piece count - count := len(pieces) - if err := binary.Write(output, binary.LittleEndian, uint64(count)); err != nil { - return nil, err - } - - // For each element - for i := range pieces { - // Encode size - if err := binary.Write(output, binary.LittleEndian, uint64(len(pieces[i]))); err != nil { - return nil, err - } - - // Encode data - if _, err := output.Write(pieces[i]); err != nil { - return nil, err - } - } - - // No error - return output.Bytes(), nil -} diff --git a/pkg/sdk/security/crypto/paseto/v4/helpers_test.go b/pkg/sdk/security/crypto/paseto/v4/helpers_test.go deleted file mode 100644 index 4e38457f..00000000 --- a/pkg/sdk/security/crypto/paseto/v4/helpers_test.go +++ /dev/null @@ -1,358 +0,0 @@ -// SPDX-FileCopyrightText: 2019 Elasticsearch B.V. -// SPDX-FileCopyrightText: 2019-2023 Thibault NORMAND -// -// SPDX-License-Identifier: Apache-2.0 AND MIT - -package v4 - -import ( - "crypto/ed25519" - "crypto/rand" - "encoding/hex" - "testing" - - "github.com/stretchr/testify/assert" -) - -// https://github.com/paseto-standard/test-vectors/blob/master/v4.json -func Test_Paseto_LocalVector(t *testing.T) { - testCases := []struct { - name string - expectFail bool - key string - nonce string - token string - payload string - footer string - implicitAssertion string - }{ - { - name: "4-E-1", - expectFail: false, - key: "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", - nonce: "0000000000000000000000000000000000000000000000000000000000000000", - token: "v4.local.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAr68PS4AXe7If_ZgesdkUMvSwscFlAl1pk5HC0e8kApeaqMfGo_7OpBnwJOAbY9V7WU6abu74MmcUE8YWAiaArVI8XJ5hOb_4v9RmDkneN0S92dx0OW4pgy7omxgf3S8c3LlQg", - payload: "{\"data\":\"this is a secret message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", - footer: "", - implicitAssertion: "", - }, - { - name: "4-E-2", - expectFail: false, - key: "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", - nonce: "0000000000000000000000000000000000000000000000000000000000000000", - token: "v4.local.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAr68PS4AXe7If_ZgesdkUMvS2csCgglvpk5HC0e8kApeaqMfGo_7OpBnwJOAbY9V7WU6abu74MmcUE8YWAiaArVI8XIemu9chy3WVKvRBfg6t8wwYHK0ArLxxfZP73W_vfwt5A", - payload: "{\"data\":\"this is a hidden message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", - footer: "", - implicitAssertion: "", - }, - { - name: "4-E-3", - expectFail: false, - nonce: "df654812bac492663825520ba2f6e67cf5ca5bdc13d4e7507a98cc4c2fcc3ad8", - key: "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", - token: "v4.local.32VIErrEkmY4JVILovbmfPXKW9wT1OdQepjMTC_MOtjA4kiqw7_tcaOM5GNEcnTxl60WkwMsYXw6FSNb_UdJPXjpzm0KW9ojM5f4O2mRvE2IcweP-PRdoHjd5-RHCiExR1IK6t6-tyebyWG6Ov7kKvBdkrrAJ837lKP3iDag2hzUPHuMKA", - payload: "{\"data\":\"this is a secret message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", - footer: "", - implicitAssertion: "", - }, - { - name: "4-E-4", - expectFail: false, - nonce: "df654812bac492663825520ba2f6e67cf5ca5bdc13d4e7507a98cc4c2fcc3ad8", - key: "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", - token: "v4.local.32VIErrEkmY4JVILovbmfPXKW9wT1OdQepjMTC_MOtjA4kiqw7_tcaOM5GNEcnTxl60WiA8rd3wgFSNb_UdJPXjpzm0KW9ojM5f4O2mRvE2IcweP-PRdoHjd5-RHCiExR1IK6t4gt6TiLm55vIH8c_lGxxZpE3AWlH4WTR0v45nsWoU3gQ", - payload: "{\"data\":\"this is a hidden message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", - footer: "", - implicitAssertion: "", - }, - { - name: "4-E-5", - expectFail: false, - nonce: "df654812bac492663825520ba2f6e67cf5ca5bdc13d4e7507a98cc4c2fcc3ad8", - key: "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", - token: "v4.local.32VIErrEkmY4JVILovbmfPXKW9wT1OdQepjMTC_MOtjA4kiqw7_tcaOM5GNEcnTxl60WkwMsYXw6FSNb_UdJPXjpzm0KW9ojM5f4O2mRvE2IcweP-PRdoHjd5-RHCiExR1IK6t4x-RMNXtQNbz7FvFZ_G-lFpk5RG3EOrwDL6CgDqcerSQ.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", - payload: "{\"data\":\"this is a secret message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", - footer: "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}", - implicitAssertion: "", - }, - { - name: "4-E-6", - expectFail: false, - nonce: "df654812bac492663825520ba2f6e67cf5ca5bdc13d4e7507a98cc4c2fcc3ad8", - key: "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", - token: "v4.local.32VIErrEkmY4JVILovbmfPXKW9wT1OdQepjMTC_MOtjA4kiqw7_tcaOM5GNEcnTxl60WiA8rd3wgFSNb_UdJPXjpzm0KW9ojM5f4O2mRvE2IcweP-PRdoHjd5-RHCiExR1IK6t6pWSA5HX2wjb3P-xLQg5K5feUCX4P2fpVK3ZLWFbMSxQ.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", - payload: "{\"data\":\"this is a hidden message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", - footer: "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}", - implicitAssertion: "", - }, - { - name: "4-E-7", - expectFail: false, - nonce: "df654812bac492663825520ba2f6e67cf5ca5bdc13d4e7507a98cc4c2fcc3ad8", - key: "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", - token: "v4.local.32VIErrEkmY4JVILovbmfPXKW9wT1OdQepjMTC_MOtjA4kiqw7_tcaOM5GNEcnTxl60WkwMsYXw6FSNb_UdJPXjpzm0KW9ojM5f4O2mRvE2IcweP-PRdoHjd5-RHCiExR1IK6t40KCCWLA7GYL9KFHzKlwY9_RnIfRrMQpueydLEAZGGcA.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", - payload: "{\"data\":\"this is a secret message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", - footer: "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}", - implicitAssertion: "{\"test-vector\":\"4-E-7\"}", - }, - { - name: "4-E-8", - expectFail: false, - nonce: "df654812bac492663825520ba2f6e67cf5ca5bdc13d4e7507a98cc4c2fcc3ad8", - key: "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", - token: "v4.local.32VIErrEkmY4JVILovbmfPXKW9wT1OdQepjMTC_MOtjA4kiqw7_tcaOM5GNEcnTxl60WiA8rd3wgFSNb_UdJPXjpzm0KW9ojM5f4O2mRvE2IcweP-PRdoHjd5-RHCiExR1IK6t5uvqQbMGlLLNYBc7A6_x7oqnpUK5WLvj24eE4DVPDZjw.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", - payload: "{\"data\":\"this is a hidden message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", - footer: "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}", - implicitAssertion: "{\"test-vector\":\"4-E-8\"}", - }, - { - name: "4-E-9", - expectFail: false, - nonce: "df654812bac492663825520ba2f6e67cf5ca5bdc13d4e7507a98cc4c2fcc3ad8", - key: "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", - token: "v4.local.32VIErrEkmY4JVILovbmfPXKW9wT1OdQepjMTC_MOtjA4kiqw7_tcaOM5GNEcnTxl60WiA8rd3wgFSNb_UdJPXjpzm0KW9ojM5f4O2mRvE2IcweP-PRdoHjd5-RHCiExR1IK6t6tybdlmnMwcDMw0YxA_gFSE_IUWl78aMtOepFYSWYfQA.YXJiaXRyYXJ5LXN0cmluZy10aGF0LWlzbid0LWpzb24", - payload: "{\"data\":\"this is a hidden message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", - footer: "arbitrary-string-that-isn't-json", - implicitAssertion: "{\"test-vector\":\"4-E-9\"}", - }, - } - - // For each testcase - for _, tc := range testCases { - testCase := tc - t.Run(testCase.name, func(t *testing.T) { - // Decode input - key, err := hex.DecodeString(testCase.key) - assert.NoError(t, err) - n, err := hex.DecodeString(testCase.nonce) - assert.NoError(t, err) - - // Encrypt - token, err := encrypt(key, n, []byte(testCase.payload), testCase.footer, testCase.implicitAssertion) - if (err != nil) != testCase.expectFail { - t.Errorf("error during the encrypt call, error = %v, wantErr %v", err, testCase.expectFail) - return - } - assert.Equal(t, testCase.token, string(token)) - - // Decrypt - message, err := Decrypt(key, []byte(testCase.token), testCase.footer, testCase.implicitAssertion) - if (err != nil) != testCase.expectFail { - t.Errorf("error during the decrypt call, error = %v, wantErr %v", err, testCase.expectFail) - return - } - assert.Equal(t, testCase.payload, string(message)) - }) - } -} - -// https://github.com/paseto-standard/test-vectors/blob/master/v4.json -func Test_Paseto_PublicVector(t *testing.T) { - testCases := []struct { - name string - expectFail bool - publicKey string - secretKey string - secretKeySeed string - secretKeyPem string - publicKeyPem string - token string - payload string - footer string - implicitAssertion string - }{ - { - name: "4-S-1", - expectFail: false, - publicKey: "1eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2", - secretKey: "b4cbfb43df4ce210727d953e4a713307fa19bb7d9f85041438d9e11b942a37741eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2", - secretKeySeed: "b4cbfb43df4ce210727d953e4a713307fa19bb7d9f85041438d9e11b942a3774", - secretKeyPem: "-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEILTL+0PfTOIQcn2VPkpxMwf6Gbt9n4UEFDjZ4RuUKjd0\n-----END PRIVATE KEY-----", - publicKeyPem: "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAHrnbu7wEfAP9cGBOAHHwmH4Wsot1ciXBHwBBXQ4gsaI=\n-----END PUBLIC KEY-----", - token: "v4.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAyMi0wMS0wMVQwMDowMDowMCswMDowMCJ9bg_XBBzds8lTZShVlwwKSgeKpLT3yukTw6JUz3W4h_ExsQV-P0V54zemZDcAxFaSeef1QlXEFtkqxT1ciiQEDA", - payload: "{\"data\":\"this is a signed message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", - footer: "", - implicitAssertion: "", - }, - { - name: "4-S-2", - expectFail: false, - publicKey: "1eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2", - secretKey: "b4cbfb43df4ce210727d953e4a713307fa19bb7d9f85041438d9e11b942a37741eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2", - secretKeySeed: "b4cbfb43df4ce210727d953e4a713307fa19bb7d9f85041438d9e11b942a3774", - secretKeyPem: "-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEILTL+0PfTOIQcn2VPkpxMwf6Gbt9n4UEFDjZ4RuUKjd0\n-----END PRIVATE KEY-----", - publicKeyPem: "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAHrnbu7wEfAP9cGBOAHHwmH4Wsot1ciXBHwBBXQ4gsaI=\n-----END PUBLIC KEY-----", - token: "v4.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAyMi0wMS0wMVQwMDowMDowMCswMDowMCJ9v3Jt8mx_TdM2ceTGoqwrh4yDFn0XsHvvV_D0DtwQxVrJEBMl0F2caAdgnpKlt4p7xBnx1HcO-SPo8FPp214HDw.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", - payload: "{\"data\":\"this is a signed message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", - footer: "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}", - implicitAssertion: "", - }, - { - name: "4-S-3", - expectFail: false, - publicKey: "1eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2", - secretKey: "b4cbfb43df4ce210727d953e4a713307fa19bb7d9f85041438d9e11b942a37741eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2", - secretKeySeed: "b4cbfb43df4ce210727d953e4a713307fa19bb7d9f85041438d9e11b942a3774", - secretKeyPem: "-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEILTL+0PfTOIQcn2VPkpxMwf6Gbt9n4UEFDjZ4RuUKjd0\n-----END PRIVATE KEY-----", - publicKeyPem: "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAHrnbu7wEfAP9cGBOAHHwmH4Wsot1ciXBHwBBXQ4gsaI=\n-----END PUBLIC KEY-----", - token: "v4.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAyMi0wMS0wMVQwMDowMDowMCswMDowMCJ9NPWciuD3d0o5eXJXG5pJy-DiVEoyPYWs1YSTwWHNJq6DZD3je5gf-0M4JR9ipdUSJbIovzmBECeaWmaqcaP0DQ.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", - payload: "{\"data\":\"this is a signed message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", - footer: "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}", - implicitAssertion: "{\"test-vector\":\"4-S-3\"}", - }, - } - - // For each testcase - for _, tc := range testCases { - testCase := tc - t.Run(testCase.name, func(t *testing.T) { - // Decode input - publicKey, err := hex.DecodeString(testCase.publicKey) - assert.NoError(t, err) - secretKey, err := hex.DecodeString(testCase.secretKey) - assert.NoError(t, err) - secretKeySeed, err := hex.DecodeString(testCase.secretKeySeed) - assert.NoError(t, err) - - // Generate ed25519 key pair - sk := ed25519.NewKeyFromSeed(secretKeySeed) - assert.Equal(t, secretKey, []byte(sk)) - pk := sk.Public().(ed25519.PublicKey) - assert.Equal(t, publicKey, []byte(pk)) - - // Sign - token, err := Sign([]byte(testCase.payload), sk, testCase.footer, testCase.implicitAssertion) - if (err != nil) != testCase.expectFail { - t.Errorf("error during the sign call, error = %v, wantErr %v", err, testCase.expectFail) - return - } - assert.Equal(t, testCase.token, string(token)) - - // Verify - message, err := Verify([]byte(testCase.token), pk, testCase.footer, testCase.implicitAssertion) - if (err != nil) != testCase.expectFail { - t.Errorf("error during the verify call, error = %v, wantErr %v", err, testCase.expectFail) - return - } - assert.Equal(t, testCase.payload, string(message)) - }) - } -} - -func Test_Paseto_Local_EncryptDecrypt(t *testing.T) { - key, err := hex.DecodeString("707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f") - assert.NoError(t, err) - - m := []byte("{\"data\":\"this is a signed message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}") - f := "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}" - i := "{\"test-vector\":\"4-S-3\"}" - - token1, err := Encrypt(rand.Reader, key, m, f, i) - assert.NoError(t, err) - assert.NotEmpty(t, token1) - - token2, err := Encrypt(rand.Reader, key, m, f, i) - assert.NoError(t, err) - assert.NotEmpty(t, token2) - - assert.NotEqual(t, token1, token2) - - p, err := Decrypt(key, token1, f, i) - assert.NoError(t, err) - assert.Equal(t, m, p) -} - -// ----------------------------------------------------------------------------- - -func benchmarkEncrypt(key, m []byte, f, i string, b *testing.B) { - for n := 0; n < b.N; n++ { - _, err := Encrypt(rand.Reader, key, m, f, i) - if err != nil { - b.Fatal(err) - } - } -} - -func Benchmark_Paseto_Encrypt(b *testing.B) { - key, err := hex.DecodeString("707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f") - assert.NoError(b, err) - - m := []byte("{\"data\":\"this is a signed message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}") - f := "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}" - i := "{\"test-vector\":\"4-S-3\"}" - - b.ReportAllocs() - b.ResetTimer() - - benchmarkEncrypt(key, m, f, i, b) -} - -func benchmarkDecrypt(key, m []byte, f, i string, b *testing.B) { - for n := 0; n < b.N; n++ { - _, err := Decrypt(key, m, f, i) - if err != nil { - b.Fatal(err) - } - } -} - -func Benchmark_Paseto_Decrypt(b *testing.B) { - key, err := hex.DecodeString("707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f") - assert.NoError(b, err) - - m := []byte("v4.local.32VIErrEkmY4JVILovbmfPXKW9wT1OdQepjMTC_MOtjA4kiqw7_tcaOM5GNEcnTxl60WiA8rd3wgFSNb_UdJPXjpzm0KW9ojM5f4O2mRvE2IcweP-PRdoHjd5-RHCiExR1IK6t5uvqQbMGlLLNYBc7A6_x7oqnpUK5WLvj24eE4DVPDZjw.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9") - f := "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}" - i := "{\"test-vector\":\"4-E-8\"}" - - b.ReportAllocs() - b.ResetTimer() - - benchmarkDecrypt(key, m, f, i, b) -} - -func benchmarkSign(m []byte, sk ed25519.PrivateKey, f, i string, b *testing.B) { - for n := 0; n < b.N; n++ { - _, err := Sign(m, sk, f, i) - if err != nil { - b.Fatal(err) - } - } -} - -func Benchmark_Paseto_Sign(b *testing.B) { - sk, err := hex.DecodeString("b4cbfb43df4ce210727d953e4a713307fa19bb7d9f85041438d9e11b942a37741eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2") - assert.NoError(b, err) - - m := []byte("{\"data\":\"this is a signed message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}") - f := "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}" - i := "{\"test-vector\":\"4-S-3\"}" - - b.ReportAllocs() - b.ResetTimer() - - benchmarkSign(m, sk, f, i, b) -} - -func benchmarkVerify(m []byte, pk ed25519.PublicKey, f, i string, b *testing.B) { - for n := 0; n < b.N; n++ { - _, err := Verify(m, pk, f, i) - if err != nil { - b.Fatal(err) - } - } -} - -func Benchmark_Paseto_Verify(b *testing.B) { - pk, err := hex.DecodeString("1eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2") - assert.NoError(b, err) - - token := []byte("v4.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAyMi0wMS0wMVQwMDowMDowMCswMDowMCJ9NPWciuD3d0o5eXJXG5pJy-DiVEoyPYWs1YSTwWHNJq6DZD3je5gf-0M4JR9ipdUSJbIovzmBECeaWmaqcaP0DQ.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9") - f := "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}" - i := "{\"test-vector\":\"4-S-3\"}" - - b.ReportAllocs() - b.ResetTimer() - - benchmarkVerify(token, pk, f, i, b) -} diff --git a/pkg/sdk/value/encryption/age/transformer.go b/pkg/sdk/value/encryption/age/transformer.go index f125e972..8874384c 100644 --- a/pkg/sdk/value/encryption/age/transformer.go +++ b/pkg/sdk/value/encryption/age/transformer.go @@ -115,7 +115,7 @@ func (d *ageTransformer) To(_ context.Context, input []byte) ([]byte, error) { } // Copy stream - if err := ioutil.Copy(ageMaxPayloadSize, w, in); err != nil { + if _, err := ioutil.LimitCopy(w, in, ageMaxPayloadSize); err != nil { return nil, err } @@ -159,7 +159,7 @@ func (d *ageTransformer) From(_ context.Context, input []byte) ([]byte, error) { } // Copy stream - if err := ioutil.Copy(ageMaxPayloadSize, &out, w); err != nil { + if _, err := ioutil.LimitCopy(&out, w, ageMaxPayloadSize); err != nil { return nil, err } diff --git a/pkg/sdk/value/encryption/paseto/transformer.go b/pkg/sdk/value/encryption/paseto/transformer.go index 973f50b0..6e5d9f2f 100644 --- a/pkg/sdk/value/encryption/paseto/transformer.go +++ b/pkg/sdk/value/encryption/paseto/transformer.go @@ -13,9 +13,9 @@ import ( "strings" "zntr.io/harp/v2/build/fips" - pasetov4 "zntr.io/harp/v2/pkg/sdk/security/crypto/paseto/v4" "zntr.io/harp/v2/pkg/sdk/value" "zntr.io/harp/v2/pkg/sdk/value/encryption" + pasetov4 "zntr.io/paseto/v4" ) func init() { @@ -38,7 +38,7 @@ func Transformer(key string) (value.Transformer, error) { } // Copy secret key - var secretKey [pasetov4.KeyLength]byte + var secretKey pasetov4.LocalKey copy(secretKey[:], k) return &pasetoTransformer{ @@ -49,14 +49,15 @@ func Transformer(key string) (value.Transformer, error) { // ----------------------------------------------------------------------------- type pasetoTransformer struct { - key [pasetov4.KeyLength]byte + key pasetov4.LocalKey } func (d *pasetoTransformer) From(_ context.Context, input []byte) ([]byte, error) { - return pasetov4.Decrypt(d.key[:], input, "", "") + return pasetov4.Decrypt(&d.key, string(input), nil, nil) } func (d *pasetoTransformer) To(_ context.Context, input []byte) ([]byte, error) { // Encrypt with paseto v4.local - return pasetov4.Encrypt(rand.Reader, d.key[:], input, "", "") + out, err := pasetov4.Encrypt(rand.Reader, &d.key, input, nil, nil) + return []byte(out), err }