diff --git a/pkg/sdk/security/crypto/hpke/api.go b/pkg/sdk/security/crypto/hpke/api.go deleted file mode 100644 index d8472661..00000000 --- a/pkg/sdk/security/crypto/hpke/api.go +++ /dev/null @@ -1,217 +0,0 @@ -// 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 deleted file mode 100644 index 15d75167..00000000 --- a/pkg/sdk/security/crypto/hpke/keyschedule.go +++ /dev/null @@ -1,190 +0,0 @@ -// 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 deleted file mode 100644 index 03381c64..00000000 --- a/pkg/sdk/security/crypto/hpke/receiver.go +++ /dev/null @@ -1,93 +0,0 @@ -// 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 deleted file mode 100644 index 9d6bd714..00000000 --- a/pkg/sdk/security/crypto/hpke/sender.go +++ /dev/null @@ -1,135 +0,0 @@ -// 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 deleted file mode 100644 index e9d77d56..00000000 --- a/pkg/sdk/security/crypto/hpke/suite.go +++ /dev/null @@ -1,88 +0,0 @@ -// 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 deleted file mode 100644 index 8871bd89..00000000 --- a/pkg/sdk/security/crypto/hpke/vector_test.go +++ /dev/null @@ -1,250 +0,0 @@ -// 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 deleted file mode 100644 index cf0be1fa..00000000 --- a/pkg/sdk/security/crypto/kem/api.go +++ /dev/null @@ -1,92 +0,0 @@ -// 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 deleted file mode 100644 index eef8ba8b..00000000 --- a/pkg/sdk/security/crypto/kem/dhkem.go +++ /dev/null @@ -1,376 +0,0 @@ -// 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 deleted file mode 100644 index 2e9d2231..00000000 --- a/pkg/sdk/security/crypto/kem/dhkem_test.go +++ /dev/null @@ -1,69 +0,0 @@ -// 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 deleted file mode 100644 index 95a405b4..00000000 --- a/pkg/sdk/security/crypto/kem/doc.go +++ /dev/null @@ -1,7 +0,0 @@ -// 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 deleted file mode 100644 index 450bd320..00000000 --- a/pkg/sdk/security/crypto/kem/key_derivation.go +++ /dev/null @@ -1,70 +0,0 @@ -// 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 deleted file mode 100644 index 26132f55..00000000 --- a/pkg/sdk/security/crypto/kem/key_derivation_test.go +++ /dev/null @@ -1,56 +0,0 @@ -// 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) - }) -}