From 925472fed45c2aa056276a62fc8d5ffc73f2902b Mon Sep 17 00:00:00 2001 From: Sun Yimin Date: Fri, 20 Sep 2024 17:39:23 +0800 Subject: [PATCH] cipher: s390x, gcm mode ghash #248 --- .github/workflows/test_s390x.yaml | 5 + cipher/gcm.go | 47 +++++ cipher/gcm_s390x.go | 285 ++++++++++++++++++++++++++++++ cipher/gcm_s390x.s | 25 +++ cipher/gcm_sm4_test.go | 11 +- 5 files changed, 368 insertions(+), 5 deletions(-) create mode 100644 cipher/gcm.go create mode 100644 cipher/gcm_s390x.go create mode 100644 cipher/gcm_s390x.s diff --git a/.github/workflows/test_s390x.yaml b/.github/workflows/test_s390x.yaml index c22eade..abd2733 100644 --- a/.github/workflows/test_s390x.yaml +++ b/.github/workflows/test_s390x.yaml @@ -39,6 +39,11 @@ jobs: env: GOARCH: ${{ matrix.arch }} + - name: Test Cipher + run: go test -v -short ./cipher/... + env: + GOARCH: ${{ matrix.arch }} + # - name: Test # run: go test -v -short ./... # env: diff --git a/cipher/gcm.go b/cipher/gcm.go new file mode 100644 index 0000000..8237b62 --- /dev/null +++ b/cipher/gcm.go @@ -0,0 +1,47 @@ + +// Copyright 2024 Sun Yimin. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +//go:build purego || !s390x + +package cipher + +import ( + _cipher "crypto/cipher" +) + +// A proxy of Golang cipher gcm mode. + +// NewGCM returns the given 128-bit, block cipher wrapped in Galois Counter Mode +// with the standard nonce length. +// +// In general, the GHASH operation performed by this implementation of GCM is not constant-time. +// An exception is when the underlying Block was created by aes.NewCipher +// on systems with hardware support for AES. See the crypto/aes package documentation for details. +func NewGCM(cipher _cipher.Block) (_cipher.AEAD, error) { + return _cipher.NewGCM(cipher) +} + +// NewGCMWithNonceSize returns the given 128-bit, block cipher wrapped in Galois +// Counter Mode, which accepts nonces of the given length. The length must not +// be zero. +// +// Only use this function if you require compatibility with an existing +// cryptosystem that uses non-standard nonce lengths. All other users should use +// NewGCM, which is faster and more resistant to misuse. +func NewGCMWithNonceSize(cipher _cipher.Block, size int) (_cipher.AEAD, error) { + return _cipher.NewGCMWithNonceSize(cipher, size) +} + +// NewGCMWithTagSize returns the given 128-bit, block cipher wrapped in Galois +// Counter Mode, which generates tags with the given length. +// +// Tag sizes between 12 and 16 bytes are allowed. +// +// Only use this function if you require compatibility with an existing +// cryptosystem that uses non-standard tag lengths. All other users should use +// NewGCM, which is more resistant to misuse. +func NewGCMWithTagSize(cipher _cipher.Block, tagSize int) (_cipher.AEAD, error) { + return _cipher.NewGCMWithTagSize(cipher, tagSize) +} diff --git a/cipher/gcm_s390x.go b/cipher/gcm_s390x.go new file mode 100644 index 0000000..9b4e5b8 --- /dev/null +++ b/cipher/gcm_s390x.go @@ -0,0 +1,285 @@ +// Copyright 2024 Sun Yimin. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +//go:build !purego + +package cipher + +import ( + _cipher "crypto/cipher" + _subtle "crypto/subtle" + "encoding/binary" + "errors" + + "github.com/emmansun/gmsm/internal/alias" + "github.com/emmansun/gmsm/internal/subtle" +) + +// gcmAble is an interface implemented by ciphers that have a specific optimized +// implementation of GCM, like crypto/aes. NewGCM will check for this interface +// and return the specific AEAD if found. +type gcmAble interface { + NewGCM(nonceSize, tagSize int) (_cipher.AEAD, error) +} + +// gcmHashKey represents the 16-byte hash key required by the GHASH algorithm. +type gcmHashKey [16]byte + +// gcm represents a Galois Counter Mode with a specific key. See +// https://csrc.nist.gov/groups/ST/toolkit/BCM/documents/proposedmodes/gcm/gcm-revised-spec.pdf +type gcm struct { + cipher _cipher.Block + nonceSize int + tagSize int + hashKey gcmHashKey +} + +// NewGCM returns the given 128-bit, block cipher wrapped in Galois Counter Mode +// with the standard nonce length. +// +// In general, the GHASH operation performed by this implementation of GCM is not constant-time. +// An exception is when the underlying Block was created by aes.NewCipher +// on systems with hardware support for AES. See the crypto/aes package documentation for details. +func NewGCM(cipher _cipher.Block) (_cipher.AEAD, error) { + return newGCMWithNonceAndTagSize(cipher, gcmStandardNonceSize, gcmTagSize) +} + +// NewGCMWithNonceSize returns the given 128-bit, block cipher wrapped in Galois +// Counter Mode, which accepts nonces of the given length. The length must not +// be zero. +// +// Only use this function if you require compatibility with an existing +// cryptosystem that uses non-standard nonce lengths. All other users should use +// NewGCM, which is faster and more resistant to misuse. +func NewGCMWithNonceSize(cipher _cipher.Block, size int) (_cipher.AEAD, error) { + return newGCMWithNonceAndTagSize(cipher, size, gcmTagSize) +} + +// NewGCMWithTagSize returns the given 128-bit, block cipher wrapped in Galois +// Counter Mode, which generates tags with the given length. +// +// Tag sizes between 12 and 16 bytes are allowed. +// +// Only use this function if you require compatibility with an existing +// cryptosystem that uses non-standard tag lengths. All other users should use +// NewGCM, which is more resistant to misuse. +func NewGCMWithTagSize(cipher _cipher.Block, tagSize int) (_cipher.AEAD, error) { + return newGCMWithNonceAndTagSize(cipher, gcmStandardNonceSize, tagSize) +} + +func newGCMWithNonceAndTagSize(cipher _cipher.Block, nonceSize, tagSize int) (_cipher.AEAD, error) { + if tagSize < gcmMinimumTagSize || tagSize > gcmBlockSize { + return nil, errors.New("cipher: incorrect tag size given to GCM") + } + + if nonceSize <= 0 { + return nil, errors.New("cipher: the nonce can't have zero length, or the security of the key will be immediately compromised") + } + + if cipher, ok := cipher.(gcmAble); ok { + return cipher.NewGCM(nonceSize, tagSize) + } + + if cipher.BlockSize() != gcmBlockSize { + return nil, errors.New("cipher: NewGCM requires 128-bit block cipher") + } + + var hk gcmHashKey + cipher.Encrypt(hk[:], hk[:]) + + g := &gcm{cipher: cipher, nonceSize: nonceSize, tagSize: tagSize, hashKey: hk} + + return g, nil +} + +const ( + gcmBlockSize = 16 + gcmTagSize = 16 + gcmMinimumTagSize = 12 // NIST SP 800-38D recommends tags with 12 or more bytes. + gcmStandardNonceSize = 12 +) + +func (g *gcm) NonceSize() int { + return g.nonceSize +} + +func (g *gcm) Overhead() int { + return g.tagSize +} + +// gcmCount represents a 16-byte big-endian count value. +type gcmCount [16]byte + +// inc increments the rightmost 32-bits of the count value by 1. +func (x *gcmCount) inc() { + binary.BigEndian.PutUint32(x[len(x)-4:], binary.BigEndian.Uint32(x[len(x)-4:])+1) +} + +// ghash uses the GHASH algorithm to hash data with the given key. The initial +// hash value is given by hash which will be updated with the new hash value. +// The length of data must be a multiple of 16-bytes. +// +//go:noescape +func ghash(key *gcmHashKey, hash *[16]byte, data []byte) + +// paddedGHASH pads data with zeroes until its length is a multiple of +// 16-bytes. It then calculates a new value for hash using the GHASH algorithm. +func (g *gcm) paddedGHASH(hash *[16]byte, data []byte) { + siz := len(data) &^ 0xf // align size to 16-bytes + if siz > 0 { + ghash(&g.hashKey, hash, data[:siz]) + data = data[siz:] + } + if len(data) > 0 { + var s [16]byte + copy(s[:], data) + ghash(&g.hashKey, hash, s[:]) + } +} + +// gcmLengths writes len0 || len1 as big-endian values to a 16-byte array. +func gcmLengths(len0, len1 uint64) [16]byte { + v := [16]byte{} + binary.BigEndian.PutUint64(v[0:], len0) + binary.BigEndian.PutUint64(v[8:], len1) + return v +} + +// auth calculates GHASH(ciphertext, additionalData), masks the result with +// tagMask and writes the result to out. +func (g *gcm) auth(out, ciphertext, additionalData []byte, tagMask *[gcmTagSize]byte) { + var hash [16]byte + g.paddedGHASH(&hash, additionalData) + g.paddedGHASH(&hash, ciphertext) + lens := gcmLengths(uint64(len(additionalData))*8, uint64(len(ciphertext))*8) + g.paddedGHASH(&hash, lens[:]) + + copy(out, hash[:]) + for i := range out { + out[i] ^= tagMask[i] + } +} + +// deriveCounter computes the initial GCM counter state from the given nonce. +// See NIST SP 800-38D, section 7.1. This assumes that counter is filled with +// zeros on entry. +func (g *gcm) deriveCounter(nonce []byte) gcmCount { + // GCM has two modes of operation with respect to the initial counter + // state: a "fast path" for 96-bit (12-byte) nonces, and a "slow path" + // for nonces of other lengths. For a 96-bit nonce, the nonce, along + // with a four-byte big-endian counter starting at one, is used + // directly as the starting counter. For other nonce sizes, the counter + // is computed by passing it through the GHASH function. + var counter gcmCount + if len(nonce) == gcmStandardNonceSize { + copy(counter[:], nonce) + counter[gcmBlockSize-1] = 1 + } else { + var hash [16]byte + g.paddedGHASH(&hash, nonce) + lens := gcmLengths(0, uint64(len(nonce))*8) + g.paddedGHASH(&hash, lens[:]) + copy(counter[:], hash[:]) + } + return counter +} + +// counterCrypt crypts in to out using g.cipher in counter mode. +func (g *gcm) counterCrypt(out, in []byte, counter *gcmCount) { + var mask [gcmBlockSize]byte + + for len(in) >= gcmBlockSize { + g.cipher.Encrypt(mask[:], counter[:]) + counter.inc() + + subtle.XORBytes(out, in, mask[:]) + out = out[gcmBlockSize:] + in = in[gcmBlockSize:] + } + + if len(in) > 0 { + g.cipher.Encrypt(mask[:], counter[:]) + counter.inc() + subtle.XORBytes(out, in, mask[:]) + } +} + +// Seal encrypts and authenticates plaintext. See the cipher.AEAD interface for +// details. +func (g *gcm) Seal(dst, nonce, plaintext, data []byte) []byte { + if len(nonce) != g.nonceSize { + panic("crypto/cipher: incorrect nonce length given to GCM") + } + if uint64(len(plaintext)) > ((1<<32)-2)*uint64(g.cipher.BlockSize()) { + panic("crypto/cipher: message too large for GCM") + } + + ret, out := alias.SliceForAppend(dst, len(plaintext)+g.tagSize) + if alias.InexactOverlap(out[:len(plaintext)], plaintext) { + panic("crypto/cipher: invalid buffer overlap") + } + + counter := g.deriveCounter(nonce) + + var tagMask [gcmBlockSize]byte + g.cipher.Encrypt(tagMask[:], counter[:]) + counter.inc() + + var tagOut [gcmTagSize]byte + g.counterCrypt(out, plaintext, &counter) + g.auth(tagOut[:], out[:len(plaintext)], data, &tagMask) + copy(out[len(plaintext):], tagOut[:]) + + return ret +} + +// Open authenticates and decrypts ciphertext. See the cipher.AEAD interface +// for details. +func (g *gcm) Open(dst, nonce, ciphertext, data []byte) ([]byte, error) { + if len(nonce) != g.nonceSize { + panic("crypto/cipher: incorrect nonce length given to GCM") + } + // Sanity check to prevent the authentication from always succeeding if an implementation + // leaves tagSize uninitialized, for example. + if g.tagSize < gcmMinimumTagSize { + panic("crypto/cipher: incorrect GCM tag size") + } + if len(ciphertext) < g.tagSize { + return nil, errOpen + } + if uint64(len(ciphertext)) > ((1<<32)-2)*uint64(g.cipher.BlockSize())+uint64(g.tagSize) { + return nil, errOpen + } + + tag := ciphertext[len(ciphertext)-g.tagSize:] + ciphertext = ciphertext[:len(ciphertext)-g.tagSize] + + counter := g.deriveCounter(nonce) + + var tagMask [gcmBlockSize]byte + g.cipher.Encrypt(tagMask[:], counter[:]) + counter.inc() + + var expectedTag [gcmTagSize]byte + g.auth(expectedTag[:], ciphertext, data, &tagMask) + + ret, out := alias.SliceForAppend(dst, len(ciphertext)) + if alias.InexactOverlap(out, ciphertext) { + panic("crypto/cipher: invalid buffer overlap") + } + + if _subtle.ConstantTimeCompare(expectedTag[:g.tagSize], tag) != 1 { + // The AESNI code decrypts and authenticates concurrently, and + // so overwrites dst in the event of a tag mismatch. That + // behavior is mimicked here in order to be consistent across + // platforms. + for i := range out { + out[i] = 0 + } + return nil, errOpen + } + + g.counterCrypt(out, ciphertext, &counter) + return ret, nil +} diff --git a/cipher/gcm_s390x.s b/cipher/gcm_s390x.s new file mode 100644 index 0000000..70c9ed0 --- /dev/null +++ b/cipher/gcm_s390x.s @@ -0,0 +1,25 @@ + +// Copyright 2016 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !purego + +#include "textflag.h" + +// func ghash(key *gcmHashKey, hash *[16]byte, data []byte) +TEXT ·ghash(SB),NOSPLIT,$32-40 + MOVD $65, R0 // GHASH function code + MOVD key+0(FP), R2 + LMG (R2), R6, R7 + MOVD hash+8(FP), R8 + LMG (R8), R4, R5 + MOVD $params-32(SP), R1 + STMG R4, R7, (R1) + LMG data+16(FP), R2, R3 // R2=base, R3=len +loop: + WORD $0xB93E0002 // compute intermediate message digest (KIMD) + BVS loop // branch back if interrupted + MVC $16, (R1), (R8) + MOVD $0, R0 + RET diff --git a/cipher/gcm_sm4_test.go b/cipher/gcm_sm4_test.go index e3796e4..04bb6e9 100644 --- a/cipher/gcm_sm4_test.go +++ b/cipher/gcm_sm4_test.go @@ -2,13 +2,14 @@ package cipher_test import ( "bytes" - "crypto/cipher" + _cipher "crypto/cipher" "crypto/rand" "encoding/hex" "fmt" "io" "testing" + "github.com/emmansun/gmsm/cipher" "github.com/emmansun/gmsm/internal/cryptotest" "github.com/emmansun/gmsm/sm4" ) @@ -243,7 +244,7 @@ func TestSM4GCM(t *testing.T) { ad, _ := hex.DecodeString(test.ad) tagSize := (len(test.result) - len(test.plaintext)) / 2 - var sm4gcm cipher.AEAD + var sm4gcm _cipher.AEAD switch { // Handle non-standard tag sizes case tagSize != 16: @@ -440,18 +441,18 @@ func TestGCMAEAD(t *testing.T) { // Test GCM with the current AES block with the standard nonce and tag // sizes. - cryptotest.TestAEAD(t, func() (cipher.AEAD, error) { return cipher.NewGCM(block) }) + cryptotest.TestAEAD(t, func() (_cipher.AEAD, error) { return cipher.NewGCM(block) }) // Test non-standard tag sizes. t.Run("MinTagSize", func(t *testing.T) { - cryptotest.TestAEAD(t, func() (cipher.AEAD, error) { return cipher.NewGCMWithTagSize(block, minTagSize) }) + cryptotest.TestAEAD(t, func() (_cipher.AEAD, error) { return cipher.NewGCMWithTagSize(block, minTagSize) }) }) // Test non-standard nonce sizes. for _, nonceSize := range []int{1, 16, 100} { t.Run(fmt.Sprintf("NonceSize-%d", nonceSize), func(t *testing.T) { - cryptotest.TestAEAD(t, func() (cipher.AEAD, error) { return cipher.NewGCMWithNonceSize(block, nonceSize) }) + cryptotest.TestAEAD(t, func() (_cipher.AEAD, error) { return cipher.NewGCMWithNonceSize(block, nonceSize) }) }) } })