yubikey/pkg/value/encryption/envelope/piv/service.go (104 lines of code) (raw):

// Licensed to Elasticsearch B.V. under one or more contributor // license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright // ownership. Elasticsearch B.V. licenses this file to you under // the Apache License, Version 2.0 (the "License"); you may // not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. package piv import ( "context" "crypto/cipher" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/sha256" "errors" "fmt" "io" "github.com/elastic/harp/pkg/sdk/security" "github.com/elastic/harp/pkg/sdk/value/encryption/envelope" "github.com/fxamacker/cbor/v2" "golang.org/x/crypto/chacha20poly1305" "golang.org/x/crypto/hkdf" ) // Service returns an PIV based envelope encryption service instance. func Service(card Card, prompt Prompter) (envelope.Service, error) { // Return service wrapper. return &service{ card: card, prompt: prompt, }, nil } const wrapLabel = "harp.elastic.co/v1/piv" type service struct { card Card prompt Prompter } // ----------------------------------------------------------------------------- func (s *service) Decrypt(ctx context.Context, encrypted []byte) ([]byte, error) { // Extract envelope var r response if err := cbor.Unmarshal(encrypted, &r); err != nil { return nil, fmt.Errorf("unable to decode envelope: %w", err) } // Extract public keys pivPublicKey := s.card.Public() pivCompressed := elliptic.MarshalCompressed(pivPublicKey.Curve, pivPublicKey.X, pivPublicKey.Y) // Identity tag tag := sha256.Sum256(pivCompressed) // Compare tag if !security.SecureCompare(tag[:4], r.Tag) { return nil, errors.New("invalid identity tag") } // Extract ephemeral public key x, y := elliptic.UnmarshalCompressed(pivPublicKey.Curve, r.EphCompressedPublic) if x == nil { return nil, errors.New("cannot unmarshal ephemeral public key") } ephPub := &ecdsa.PublicKey{ Curve: pivPublicKey.Curve, X: x, Y: y, } // Compute shared secret sharedSecret, err := s.card.SharedKey(ephPub, s.prompt) if err != nil { return nil, fmt.Errorf("unable to compute shared secret: %w", err) } // Derive AEAD cipher from parameters aead, err := s.deriveAEAD(r.EphCompressedPublic, pivCompressed, sharedSecret, chacha20poly1305.KeySize) if err != nil { return nil, fmt.Errorf("unable to initialize aead: %w", err) } // Decrypt // Use fixed nonce to save space and also sharedsecret is derived from ephemeral // key that act as nonce. nonce := make([]byte, chacha20poly1305.NonceSize) clearText, err := aead.Open(nil, nonce, r.Payload, nil) if err != nil { return nil, fmt.Errorf("unable to decrypt payload: %w", err) } // No error return clearText, nil } func (s *service) Encrypt(ctx context.Context, cleartext []byte) ([]byte, error) { // Extract public keys pivPublicKey := s.card.Public() pivCompressed := elliptic.MarshalCompressed(pivPublicKey.Curve, pivPublicKey.X, pivPublicKey.Y) // Identity tag tag := sha256.Sum256(pivCompressed) // Generate ephemeral key eph, err := ecdsa.GenerateKey(pivPublicKey.Curve, rand.Reader) if err != nil { return nil, fmt.Errorf("unable to generate ephemeral key pair: %w", err) } ephCompressed := elliptic.MarshalCompressed(eph.Curve, eph.PublicKey.X, eph.PublicKey.Y) // ECDH shared secret between ephemeral key and yubikey sharedSecretNum, _ := eph.PublicKey.ScalarMult(s.card.Public().X, s.card.Public().Y, eph.D.Bytes()) sharedSecret := sharedSecretNum.Bytes() // Derive AEAD cipher from parameters aead, err := s.deriveAEAD(ephCompressed, pivCompressed, sharedSecret, chacha20poly1305.KeySize) if err != nil { return nil, fmt.Errorf("unable to initialize aead: %w", err) } // Encrypt // Use fixed nonce to save space and also sharedsecret is derived from ephemeral // key that act as nonce. nonce := make([]byte, chacha20poly1305.NonceSize) // Return encrypted content body, err := cbor.Marshal(response{ EphCompressedPublic: ephCompressed, Tag: tag[:4], Payload: aead.Seal(nil, nonce, cleartext, nil), }) if err != nil { return nil, fmt.Errorf("unable to encode envelope: %w", err) } // No error return body, nil } // ----------------------------------------------------------------------------- func (s *service) deriveAEAD(ephPub, pivPub, sharedSecret []byte, keySize int) (cipher.AEAD, error) { // EphemeralPubKey || YubikeyPubKey salt := make([]byte, 0, len(ephPub)+len(pivPub)) salt = append(salt, ephPub...) salt = append(salt, pivPub...) // Stretch sharedsecret to required size. h := hkdf.New(sha256.New, sharedSecret, salt, []byte(wrapLabel)) wrappingKey := make([]byte, keySize) if _, errRand := io.ReadFull(h, wrappingKey); errRand != nil { return nil, fmt.Errorf("unable to generate wrapping key: %w", errRand) } // Prepare AEAD encryption aead, err := chacha20poly1305.New(wrappingKey) if err != nil { return nil, fmt.Errorf("unabe to initialize AEAD: %w", err) } // No error return aead, nil }