Archived
110 lines
3.3 KiB
Go
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
|
|
}
|