This repository has been archived on 2026-02-07. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
2026-01-07 09:17:03 +01:00

110 lines
3.3 KiB
Go

package main
import (
"crypto/ed25519"
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"fmt"
"strings"
)
const (
opensshEd25519Prefix = "ssh-ed25519"
ed25519PublicKeySize = 32
)
// ParseOpenSSHEd25519PublicKey parses an OpenSSH formatted ed25519 public key.
// Format: "ssh-ed25519 AAAA..." (base64 encoded key data)
func ParseOpenSSHEd25519PublicKey(keyStr string) (ed25519.PublicKey, error) {
parts := strings.Fields(keyStr)
if len(parts) < 2 {
return nil, fmt.Errorf("invalid OpenSSH key format: expected 'ssh-ed25519 <base64-data>'")
}
if parts[0] != opensshEd25519Prefix {
return nil, fmt.Errorf("unsupported key type: %s (expected %s)", parts[0], opensshEd25519Prefix)
}
keyData, err := base64.StdEncoding.DecodeString(parts[1])
if err != nil {
return nil, fmt.Errorf("failed to decode base64 key data: %w", err)
}
// OpenSSH wire format: length-prefixed strings
// First: 4 bytes length + "ssh-ed25519"
// Then: 4 bytes length + 32-byte public key
return parseOpenSSHWireFormat(keyData)
}
func parseOpenSSHWireFormat(data []byte) (ed25519.PublicKey, error) {
if len(data) < 4 {
return nil, fmt.Errorf("key data too short")
}
// Read key type length and value
typeLen := binary.BigEndian.Uint32(data[:4])
if len(data) < int(4+typeLen+4) {
return nil, fmt.Errorf("key data too short for type field")
}
keyType := string(data[4 : 4+typeLen])
if keyType != opensshEd25519Prefix {
return nil, fmt.Errorf("unexpected key type in wire format: %s", keyType)
}
// Read public key length and value
offset := 4 + typeLen
pubKeyLen := binary.BigEndian.Uint32(data[offset : offset+4])
offset += 4
if pubKeyLen != ed25519PublicKeySize {
return nil, fmt.Errorf("unexpected public key size: %d (expected %d)", pubKeyLen, ed25519PublicKeySize)
}
if len(data) < int(offset)+ed25519PublicKeySize {
return nil, fmt.Errorf("key data too short for public key")
}
pubKey := make([]byte, ed25519PublicKeySize)
copy(pubKey, data[offset:offset+ed25519PublicKeySize])
return ed25519.PublicKey(pubKey), nil
}
// KeyFingerprint returns a hex-encoded SHA256 fingerprint of the public key (first 16 chars).
func KeyFingerprint(key ed25519.PublicKey) string {
hash := sha256.Sum256(key)
return fmt.Sprintf("%x", hash[:8]) // 8 bytes = 16 hex chars
}
// VerifySignature checks if the signature is valid for the given data, filename, and timestamp.
// It tries each allowed key and returns true if any key verifies.
// The signature is over: int64_be(timestamp) || filename || data
func VerifySignature(data []byte, filename string, timestamp int64, signature []byte, allowedKeys []ed25519.PublicKey) bool {
if len(signature) != ed25519.SignatureSize {
return false
}
message := buildSignedMessage(timestamp, filename, data)
for _, key := range allowedKeys {
if ed25519.Verify(key, message, signature) {
return true
}
}
return false
}
// buildSignedMessage constructs the message that should be signed.
// Format: 8 bytes timestamp (big-endian int64) || filename bytes || data bytes
func buildSignedMessage(timestamp int64, filename string, data []byte) []byte {
filenameBytes := []byte(filename)
message := make([]byte, 8+len(filenameBytes)+len(data))
binary.BigEndian.PutUint64(message[:8], uint64(timestamp))
copy(message[8:], filenameBytes)
copy(message[8+len(filenameBytes):], data)
return message
}