Skip to content

Commit

Permalink
feat(v2): Refactor entity generation and force hash selection for sig…
Browse files Browse the repository at this point in the history
…ning algorithms
  • Loading branch information
lubux committed Oct 5, 2023
1 parent 869b533 commit d819a77
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 39 deletions.
1 change: 1 addition & 0 deletions openpgp/ed25519/ed25519.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ func GenerateKey(rand io.Reader) (*PrivateKey, error) {
}

// Sign signs a message with the ed25519 algorithm.
// priv MUST be a valid key! Check this with Validate() before use.
func Sign(priv *PrivateKey, message []byte) ([]byte, error) {
return ed25519lib.Sign(priv.Key, message), nil
}
Expand Down
1 change: 1 addition & 0 deletions openpgp/ed448/ed448.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ func GenerateKey(rand io.Reader) (*PrivateKey, error) {
}

// Sign signs a message with the ed448 algorithm.
// priv MUST be a valid key! Check this with Validate() before use.
func Sign(priv *PrivateKey, message []byte) ([]byte, error) {
// Ed448 is used with the empty string as a context string.
// See https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-crypto-refresh-08#section-13.7
Expand Down
93 changes: 66 additions & 27 deletions openpgp/v2/key_generation.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ type userIdData struct {
name, comment, email string
}

type keyProperties struct {
primaryKey *packet.PrivateKey
creationTime time.Time
keyLifetimeSecs uint32
hash crypto.Hash
cipher packet.CipherFunction
aead *packet.AEADConfig
compression packet.CompressionAlgo
}

// NewEntityWithoutId returns an Entity that contains fresh keys for signing and
// encrypting pgp messages. The key is not associated with an identity.
// This is only allowed for v6 key generation. If v6 is not enabled,
Expand All @@ -48,13 +58,23 @@ func NewEntity(name, comment, email string, config *packet.Config) (*Entity, err
return newEntity(&userIdData{name, comment, email}, config)
}

func selectKeyProperties(creationTime time.Time, config *packet.Config, primary *packet.PrivateKey) *keyProperties {
return &keyProperties{
primaryKey: primary,
creationTime: creationTime,
keyLifetimeSecs: config.KeyLifetime(),
hash: config.Hash(),
cipher: config.Cipher(),
aead: config.AEAD(),
compression: config.Compression(),
}
}

func newEntity(uid *userIdData, config *packet.Config) (*Entity, error) {
if uid == nil && !config.V6() {
return nil, errors.InvalidArgumentError("user id has to be set for non-v6 keys")
}

creationTime := config.Now()
keyLifetimeSecs := config.KeyLifetime()

// Generate a primary signing key
primaryPrivRaw, err := newSigner(config)
Expand All @@ -66,6 +86,8 @@ func newEntity(uid *userIdData, config *packet.Config) (*Entity, error) {
primary.UpgradeToV6()
}

keyProperties := selectKeyProperties(creationTime, config, primary)

e := &Entity{
PrimaryKey: &primary.PublicKey,
PrivateKey: primary,
Expand All @@ -75,10 +97,12 @@ func newEntity(uid *userIdData, config *packet.Config) (*Entity, error) {
}

if config.V6() {
e.AddDirectKeySignature(config)
e.AddDirectKeySignature(keyProperties, config)
keyProperties = nil
}

if uid != nil {
err = e.addUserId(*uid, config, creationTime, keyLifetimeSecs, !config.V6())
err = e.addUserId(*uid, config, keyProperties)
if err != nil {
return nil, err
}
Expand All @@ -95,16 +119,16 @@ func newEntity(uid *userIdData, config *packet.Config) (*Entity, error) {
}

func (t *Entity) AddUserId(name, comment, email string, config *packet.Config) error {
creationTime := config.Now()
keyLifetimeSecs := config.KeyLifetime()
return t.addUserId(userIdData{name, comment, email}, config, creationTime, keyLifetimeSecs, !config.V6())
var keyProperties *keyProperties
if !config.V6() {
keyProperties = selectKeyProperties(config.Now(), config, t.PrivateKey)
}
return t.addUserId(userIdData{name, comment, email}, config, keyProperties)
}

func (t *Entity) AddDirectKeySignature(config *packet.Config) error {
func (t *Entity) AddDirectKeySignature(selectedKeyProperties *keyProperties, config *packet.Config) error {
selfSignature := createSignaturePacket(&t.PrivateKey.PublicKey, packet.SigTypeDirectSignature, config)
creationTime := config.Now()
keyLifetimeSecs := config.KeyLifetime()
err := writeKeyProperties(selfSignature, creationTime, keyLifetimeSecs, config)
err := writeKeyProperties(selfSignature, selectedKeyProperties)
if err != nil {
return err
}
Expand All @@ -116,30 +140,45 @@ func (t *Entity) AddDirectKeySignature(config *packet.Config) error {
return nil
}

func writeKeyProperties(selfSignature *packet.Signature, creationTime time.Time, keyLifetimeSecs uint32, config *packet.Config) error {
selfSignature.CreationTime = creationTime
selfSignature.KeyLifetimeSecs = &keyLifetimeSecs
func writeKeyProperties(selfSignature *packet.Signature, selectedKeyProperties *keyProperties) error {
selfSignature.CreationTime = selectedKeyProperties.creationTime
selfSignature.KeyLifetimeSecs = &selectedKeyProperties.keyLifetimeSecs
selfSignature.FlagsValid = true
selfSignature.FlagSign = true
selfSignature.FlagCertify = true
selfSignature.SEIPDv1 = true // true by default, see 5.8 vs. 5.14
selfSignature.SEIPDv2 = config.AEAD() != nil
selfSignature.SEIPDv2 = selectedKeyProperties.aead != nil

// Set the PreferredHash for the SelfSignature from the packet.Config.
// If it is not the must-implement algorithm from rfc4880bis, append that.
hash, ok := algorithm.HashToHashId(config.Hash())
hash, ok := algorithm.HashToHashId(selectedKeyProperties.hash)
if !ok {
return errors.UnsupportedError("unsupported preferred hash function")
}

selfSignature.PreferredHash = []uint8{hash}
if config.Hash() != crypto.SHA256 {
selfSignature.PreferredHash = []uint8{}
// Ensure that for signing algorithms with higher security level an
// appropriate a matching hash function is available.
acceptableHashes := acceptableHashesToWrite(&selectedKeyProperties.primaryKey.PublicKey)
var match bool
for _, acceptableHashes := range acceptableHashes {
if acceptableHashes == hash {
match = true
break
}
}
if !match && len(acceptableHashes) > 0 {
selfSignature.PreferredHash = []uint8{acceptableHashes[0]}
}

selfSignature.PreferredHash = append(selfSignature.PreferredHash, hash)
if selectedKeyProperties.hash != crypto.SHA256 {
selfSignature.PreferredHash = append(selfSignature.PreferredHash, hashToHashId(crypto.SHA256))
}

// Likewise for DefaultCipher.
selfSignature.PreferredSymmetric = []uint8{uint8(config.Cipher())}
if config.Cipher() != packet.CipherAES128 {
selfSignature.PreferredSymmetric = []uint8{uint8(selectedKeyProperties.cipher)}
if selectedKeyProperties.cipher != packet.CipherAES128 {
selfSignature.PreferredSymmetric = append(selfSignature.PreferredSymmetric, uint8(packet.CipherAES128))
}

Expand All @@ -148,13 +187,13 @@ func writeKeyProperties(selfSignature *packet.Signature, creationTime time.Time,
// DefaultCompressionAlgo if any is set (to signal support for cases
// where the application knows that using compression is safe).
selfSignature.PreferredCompression = []uint8{uint8(packet.CompressionNone)}
if config.Compression() != packet.CompressionNone {
selfSignature.PreferredCompression = append(selfSignature.PreferredCompression, uint8(config.Compression()))
if selectedKeyProperties.compression != packet.CompressionNone {
selfSignature.PreferredCompression = append(selfSignature.PreferredCompression, uint8(selectedKeyProperties.compression))
}

// And for DefaultMode.
modes := []uint8{uint8(config.AEAD().Mode())}
if config.AEAD().Mode() != packet.AEADModeOCB {
modes := []uint8{uint8(selectedKeyProperties.aead.Mode())}
if selectedKeyProperties.aead.Mode() != packet.AEADModeOCB {
modes = append(modes, uint8(packet.AEADModeOCB))
}

Expand All @@ -167,7 +206,7 @@ func writeKeyProperties(selfSignature *packet.Signature, creationTime time.Time,
return nil
}

func (t *Entity) addUserId(userIdData userIdData, config *packet.Config, creationTime time.Time, keyLifetimeSecs uint32, writeProperties bool) error {
func (t *Entity) addUserId(userIdData userIdData, config *packet.Config, selectedKeyProperties *keyProperties) error {
uid := packet.NewUserId(userIdData.name, userIdData.comment, userIdData.email)
if uid == nil {
return errors.InvalidArgumentError("user id field contained invalid characters")
Expand All @@ -180,8 +219,8 @@ func (t *Entity) addUserId(userIdData userIdData, config *packet.Config, creatio
primary := t.PrivateKey
isPrimaryId := len(t.Identities) == 0
selfSignature := createSignaturePacket(&primary.PublicKey, packet.SigTypePositiveCert, config)
if writeProperties {
err := writeKeyProperties(selfSignature, creationTime, keyLifetimeSecs, config)
if selectedKeyProperties != nil {
err := writeKeyProperties(selfSignature, selectedKeyProperties)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion openpgp/v2/keys_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -829,7 +829,7 @@ func TestNewEntityWithDefaultHash(t *testing.T) {
t.Fatal("didn't find a preferred hash list in self signature")
}
ph := hashToHashId(c.DefaultHash)
if prefs[0] != ph {
if c.DefaultHash != crypto.SHA224 && prefs[0] != ph {
t.Fatalf("Expected preferred hash to be %d, got %d", ph, prefs[0])
}
}
Expand Down
23 changes: 23 additions & 0 deletions openpgp/v2/keys_v6_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,3 +201,26 @@ func TestNewEntityWithDefaultHashV6(t *testing.T) {
}
}
}

func TestKeyGenerationHighSecurityLevel(t *testing.T) {
c := &packet.Config{
V6Keys: true,
Algorithm: packet.PubKeyAlgoEd448,
DefaultHash: crypto.SHA256,
}
entity, err := NewEntity("Golang Gopher", "Test Key", "[email protected]", c)
if err != nil {
t.Fatal(err)
}
selfSig, err := entity.PrimarySelfSignature(time.Time{})
if err != nil {
t.Fatal(err)
}
if !(selfSig.PreferredHash[0] == hashToHashId(crypto.SHA512)) {
t.Fatal("sha 512 should be the preferred option")
}
if selfSig.Hash != crypto.SHA512 {
t.Fatal("sha 512 should be used in self signatures")
}

}
90 changes: 79 additions & 11 deletions openpgp/v2/write.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ func detachSignWithWriter(w io.Writer, signers []*Entity, sigType packet.Signatu
candidateHashes = intersectPreferences(candidateHashes, preferredHashes)

var hash crypto.Hash
if hash, err = selectHash(candidateHashes, config.Hash()); err != nil {
if hash, err = selectHash(candidateHashes, config.Hash(), signingKey.PrivateKey); err != nil {
return
}

Expand Down Expand Up @@ -454,7 +454,12 @@ func writeAndSign(payload io.WriteCloser, candidateHashes [][]uint8, signEntitie
sigContext := signatureContext{
signer: signer,
}
hash, err := selectHash(candidateHashes[signEntityIdx], config.Hash())

if signKey.PrimarySelfSignature == nil {
return nil, errors.InvalidArgumentError("signing key has no self-signature")
}
candidateHashes[signEntityIdx] = intersectPreferences(candidateHashes[signEntityIdx], signKey.PrimarySelfSignature.PreferredHash)
hash, err := selectHash(candidateHashes[signEntityIdx], config.Hash(), signKey.PrivateKey)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -849,13 +854,35 @@ func (s signatureWriter) Close() error {
return s.encryptedData.Close()
}

func adaptHashToSigningKey(config *packet.Config, primary *packet.PublicKey) crypto.Hash {
acceptableHashes := acceptableHashesToWrite(primary)
hash, ok := algorithm.HashToHashId(config.Hash())
if !ok {
return config.Hash()
}
for _, acceptableHashes := range acceptableHashes {
if acceptableHashes == hash {
return config.Hash()
}
}
if len(acceptableHashes) > 0 {
defaultAcceptedHash, ok := algorithm.HashIdToHash(acceptableHashes[0])
if !ok {
return config.Hash()
}
return defaultAcceptedHash
}
return config.Hash()
}

func createSignaturePacket(signer *packet.PublicKey, sigType packet.SignatureType, config *packet.Config) *packet.Signature {
sigLifetimeSecs := config.SigLifetime()
hash := adaptHashToSigningKey(config, signer)
return &packet.Signature{
Version: signer.Version,
SigType: sigType,
PubKeyAlgo: signer.PubKeyAlgo,
Hash: config.Hash(),
Hash: hash,
CreationTime: config.Now(),
IssuerKeyId: &signer.KeyId,
IssuerFingerprint: signer.Fingerprint,
Expand Down Expand Up @@ -911,7 +938,10 @@ func handleCompression(compressed io.WriteCloser, candidateCompression []uint8,
}

// selectHash selects the preferred hash given the candidateHashes and the configuredHash
func selectHash(candidateHashes []byte, configuredHash crypto.Hash) (hash crypto.Hash, err error) {
func selectHash(candidateHashes []byte, configuredHash crypto.Hash, signer *packet.PrivateKey) (hash crypto.Hash, err error) {
acceptableHashes := acceptableHashesToWrite(&signer.PublicKey)
candidateHashes = intersectPreferences(acceptableHashes, candidateHashes)

for _, hashId := range candidateHashes {
if h, ok := algorithm.HashIdToHash(hashId); ok && h.Available() {
hash = h
Expand All @@ -930,20 +960,22 @@ func selectHash(candidateHashes []byte, configuredHash crypto.Hash) (hash crypto
}

if hash == 0 {
hashId := candidateHashes[0]
name, ok := algorithm.HashIdToString(hashId)
if !ok {
name = "#" + strconv.Itoa(int(hashId))
if len(acceptableHashes) > 0 {
if h, ok := algorithm.HashIdToHash(acceptableHashes[0]); ok {
hash = h
} else {
return 0, errors.InvalidArgumentError("no candidate hash functions are compiled in.")
}
} else {
return 0, errors.InvalidArgumentError("no candidate hash functions are compiled in.")
}
return 0, errors.InvalidArgumentError("no candidate hash functions are compiled in. (Wanted " + name + " in this case.)")
}
return
}

func parseOutsideSig(outsideSig []byte) (outSigPacket *packet.Signature, err error) {
var p packet.Packet
packets := packet.NewReader(bytes.NewReader(outsideSig))
p, err = packets.Next()
p, err := packets.Next()
if goerrors.Is(err, io.EOF) {
return nil, errors.ErrUnknownIssuer
}
Expand All @@ -961,3 +993,39 @@ func parseOutsideSig(outsideSig []byte) (outSigPacket *packet.Signature, err err
}
return outSigPacket, nil
}

func acceptableHashesToWrite(singingKey *packet.PublicKey) []uint8 {
switch singingKey.PubKeyAlgo {
case packet.PubKeyAlgoEd448:
return []uint8{
hashToHashId(crypto.SHA512),
hashToHashId(crypto.SHA3_512),
}
case packet.PubKeyAlgoECDSA:
if curve, err := singingKey.Curve(); err == nil {
if curve == packet.Curve448 ||
curve == packet.CurveNistP521 ||
curve == packet.CurveBrainpoolP512 {
return []uint8{
hashToHashId(crypto.SHA512),
hashToHashId(crypto.SHA3_512),
}
} else if curve == packet.CurveBrainpoolP384 ||
curve == packet.CurveNistP384 {
return []uint8{
hashToHashId(crypto.SHA256),
hashToHashId(crypto.SHA384),
hashToHashId(crypto.SHA512),
hashToHashId(crypto.SHA3_512),
}
}
}
}
return []uint8{
hashToHashId(crypto.SHA256),
hashToHashId(crypto.SHA384),
hashToHashId(crypto.SHA512),
hashToHashId(crypto.SHA3_256),
hashToHashId(crypto.SHA3_512),
}
}

0 comments on commit d819a77

Please sign in to comment.