//go:build !gui // +build !gui package main import ( "bufio" "crypto" "crypto/aes" "crypto/cipher" "crypto/ecdsa" "crypto/ed25519" "crypto/elliptic" "crypto/rand" "crypto/rsa" "crypto/sha256" "crypto/sha512" "crypto/x509" "encoding/asn1" "encoding/base64" "encoding/hex" "encoding/pem" "errors" "flag" "fmt" "io" "math/big" "os" "regexp" "strings" "github.com/awnumar/memguard" "github.com/go-piv/piv-go/v2/piv" ) type ecSignature struct{ R, S *big.Int } const ( AlgorithmECCP256 = "ECCP256" AlgorithmECCP384 = "ECCP384" AlgorithmED25519 = "ED25519" ) const ( Ed25519SignatureSize = 64 Ed25519PublicKeySize = 32 Ed25519CombinedSize = Ed25519SignatureSize + Ed25519PublicKeySize minRSABits = 2048 ) var supportedAlgorithms = map[string]bool{ AlgorithmECCP256: true, AlgorithmECCP384: true, AlgorithmED25519: true, } var curveToAlgorithm = map[elliptic.Curve]string{ elliptic.P256(): AlgorithmECCP256, elliptic.P384(): AlgorithmECCP384, } var curveToHash = map[elliptic.Curve]crypto.Hash{ elliptic.P256(): crypto.SHA256, elliptic.P384(): crypto.SHA384, } var supportedRSASizes = map[int]string{ 2048: "RSA2048", 3072: "RSA3072", 4096: "RSA4096", } var yubiKeyIndex int type signatureInfo struct { Algorithm string OriginalMessage []byte PublicKey []byte } func main() { defer memguard.Purge() if len(os.Args) == 1 { printCLIHelp(os.Stdout) return } if err := runCLI(os.Args[1:]); err != nil { fmt.Fprintln(os.Stderr, "Error:", err) os.Exit(1) } } func runCLI(args []string) error { if len(args) == 0 { printCLIHelp(os.Stdout) return nil } switch args[0] { case "help", "-h", "--help": printCLIHelp(os.Stdout) return nil case "version", "-v", "--version": fmt.Println("yubicrypt v0.2.0") return nil case "encrypt", "enc": return runEncryptCLI(args[1:]) case "decrypt", "dec": return runDecryptCLI(args[1:]) case "sign": return runSignCLI(args[1:]) case "verify": return runVerifyCLI(args[1:]) case "pad": return runPadCLI(args[1:]) case "unpad": return runUnpadCLI(args[1:]) case "cards": return runCardsCLI() default: return fmt.Errorf("unknown command %q; run `yubicrypt help`", args[0]) } } func printCLIHelp(w io.Writer) { _, _ = io.WriteString(w, `Usage: yubicrypt [options] Commands: encrypt Encrypt with an RSA public certificate/key decrypt Decrypt with YubiKey PIV slot 9d sign Sign with YubiKey PIV slot 9c verify Verify an embedded yubicrypt signature pad ISO/IEC 7816-4 pad and Base64 encode unpad Base64 decode and remove padding cards List visible YubiKeys version Print version Common IO options: -i, --input FILE input file, or - for stdin (default -) -o, --output FILE output file, or - for stdout (default -) --text TEXT use TEXT instead of reading input --quiet suppress status lines on stderr PIN options for decrypt/sign: --pin-stdin read the PIV PIN from the first stdin line --pin-file FILE read the PIV PIN from the first line of FILE --pin PIN use PIN directly (less safe: shell history/process list) --card-index N YubiKey index from yubicrypt cards (default 0) Examples: yubicrypt encrypt --key alice.crt -i msg.txt -o msg.yc printf '%s\n' "$PIV_PIN" | yubicrypt decrypt --pin-stdin -i msg.yc -o msg.txt printf '%s\n' "$PIV_PIN" | yubicrypt sign --pin-stdin -i msg.txt -o msg.sig.txt yubicrypt verify -i msg.sig.txt --public-key `) } func newFlagSet(name string) *flag.FlagSet { fs := flag.NewFlagSet(name, flag.ContinueOnError) fs.SetOutput(os.Stderr) return fs } func addIOFlags(fs *flag.FlagSet, input, output, text *string, quiet *bool) { fs.StringVar(input, "input", "-", "input file, or - for stdin") fs.StringVar(input, "i", "-", "input file, or - for stdin") fs.StringVar(output, "output", "-", "output file, or - for stdout") fs.StringVar(output, "o", "-", "output file, or - for stdout") fs.StringVar(text, "text", "", "literal input text") fs.BoolVar(quiet, "quiet", false, "suppress status lines on stderr") } func addPINFlags(fs *flag.FlagSet, pin, pinFile *string, pinStdin *bool) { fs.StringVar(pin, "pin", "", "PIV PIN (less safe: shell history/process list)") fs.StringVar(pinFile, "pin-file", "", "file containing the PIV PIN on the first line") fs.BoolVar(pinStdin, "pin-stdin", false, "read the PIV PIN from the first stdin line") fs.IntVar(&yubiKeyIndex, "card-index", 0, "YubiKey index from `yubicrypt cards`") } func runEncryptCLI(args []string) error { var input, output, text, key string var quiet bool fs := newFlagSet("encrypt") addIOFlags(fs, &input, &output, &text, &quiet) fs.StringVar(&key, "key", "", "RSA public certificate/key file") if err := fs.Parse(args); err != nil { return err } if key == "" { return fmt.Errorf("encrypt requires --key") } stdin := bufio.NewReader(os.Stdin) data, err := readInput(input, text, stdin) if err != nil { return fmt.Errorf("read input: %w", err) } result, err := encryptData(data, key) if err != nil { return err } if err := writeOutput(output, []byte(result)); err != nil { return fmt.Errorf("write output: %w", err) } logStatus(quiet, "encrypted %d bytes", len(data)) return nil } func runDecryptCLI(args []string) error { var input, output, text, pin, pinFile string var quiet, pinStdin bool fs := newFlagSet("decrypt") addIOFlags(fs, &input, &output, &text, &quiet) addPINFlags(fs, &pin, &pinFile, &pinStdin) if err := fs.Parse(args); err != nil { return err } stdin := bufio.NewReader(os.Stdin) pivPIN, err := readPIN(pin, pinFile, pinStdin, stdin) if err != nil { return err } data, err := readInput(input, text, stdin) if err != nil { return fmt.Errorf("read input: %w", err) } result, err := decryptData(data, pivPIN) if err != nil { return err } if err := writeOutput(output, result); err != nil { return fmt.Errorf("write output: %w", err) } logStatus(quiet, "decrypted %d bytes", len(result)) return nil } func runSignCLI(args []string) error { var input, output, text, pin, pinFile string var quiet, pinStdin, force bool fs := newFlagSet("sign") addIOFlags(fs, &input, &output, &text, &quiet) addPINFlags(fs, &pin, &pinFile, &pinStdin) fs.BoolVar(&force, "force", false, "allow signing input that already contains a yubicrypt signature") if err := fs.Parse(args); err != nil { return err } stdin := bufio.NewReader(os.Stdin) pivPIN, err := readPIN(pin, pinFile, pinStdin, stdin) if err != nil { return err } data, err := readInput(input, text, stdin) if err != nil { return fmt.Errorf("read input: %w", err) } if !force && containsSignatureBlock(data) { return fmt.Errorf("input already contains a yubicrypt signature; use --force to sign anyway") } result, err := signData(data, pivPIN) if err != nil { return err } if err := writeOutput(output, []byte(result)); err != nil { return fmt.Errorf("write output: %w", err) } logStatus(quiet, "signed %d bytes", len(data)) return nil } func runVerifyCLI(args []string) error { var input, output, text, messageOutput string var quiet, publicKeyOnly bool fs := newFlagSet("verify") fs.StringVar(&input, "input", "-", "input file, or - for stdin") fs.StringVar(&input, "i", "-", "input file, or - for stdin") fs.StringVar(&text, "text", "", "literal input text") fs.BoolVar(&quiet, "quiet", false, "suppress success output") fs.BoolVar(&publicKeyOnly, "public-key", false, "print embedded public key hex only") fs.StringVar(&messageOutput, "message-output", "", "write verified original message to file") fs.StringVar(&output, "output", "", "alias for --message-output") fs.StringVar(&output, "o", "", "alias for --message-output") if err := fs.Parse(args); err != nil { return err } if output != "" && messageOutput == "" { messageOutput = output } stdin := bufio.NewReader(os.Stdin) data, err := readInput(input, text, stdin) if err != nil { return fmt.Errorf("read input: %w", err) } info, err := verifySignedData(data) if err != nil { return err } if messageOutput != "" { if err := writeOutput(messageOutput, info.OriginalMessage); err != nil { return fmt.Errorf("write verified message: %w", err) } } if publicKeyOnly { fmt.Fprintln(os.Stdout, hex.EncodeToString(info.PublicKey)) return nil } if !quiet { fmt.Fprintf(os.Stdout, "OK %s signature valid; message=%d bytes; public_key=%s\n", info.Algorithm, len(info.OriginalMessage), hex.EncodeToString(info.PublicKey)) } return nil } func runPadCLI(args []string) error { var input, output, text string var quiet bool fs := newFlagSet("pad") addIOFlags(fs, &input, &output, &text, &quiet) if err := fs.Parse(args); err != nil { return err } stdin := bufio.NewReader(os.Stdin) data, err := readInput(input, text, stdin) if err != nil { return fmt.Errorf("read input: %w", err) } padded := securePadMessage(data) encoded := formatBase64RFC(base64.StdEncoding.EncodeToString(padded)) if err := writeOutput(output, []byte(encoded)); err != nil { return fmt.Errorf("write output: %w", err) } logStatus(quiet, "padded %d -> %d bytes", len(data), len(padded)) return nil } func runUnpadCLI(args []string) error { var input, output, text string var quiet bool fs := newFlagSet("unpad") addIOFlags(fs, &input, &output, &text, &quiet) if err := fs.Parse(args); err != nil { return err } stdin := bufio.NewReader(os.Stdin) data, err := readInput(input, text, stdin) if err != nil { return fmt.Errorf("read input: %w", err) } decoded, err := decodeBase64Clean(data) if err != nil { return err } defer memguard.WipeBytes(decoded) result, err := secureUnpadMessage(decoded) if err != nil { return err } if err := writeOutput(output, result); err != nil { return fmt.Errorf("write output: %w", err) } logStatus(quiet, "unpadded %d bytes", len(result)) return nil } func runCardsCLI() error { cards, err := piv.Cards() if err != nil { return fmt.Errorf("list cards: %w", err) } if len(cards) == 0 { return fmt.Errorf("no smart card found") } for i, card := range cards { fmt.Fprintf(os.Stdout, "%d\t%s\n", i, card) } return nil } func readInput(path, text string, stdin *bufio.Reader) ([]byte, error) { if text != "" { return []byte(text), nil } if path == "" || path == "-" { return io.ReadAll(stdin) } return os.ReadFile(path) } func writeOutput(path string, data []byte) error { if path == "" || path == "-" { _, err := os.Stdout.Write(data) return err } return os.WriteFile(path, data, 0600) } func firstLine(data []byte) string { line := string(data) if i := strings.IndexAny(line, "\r\n"); i >= 0 { line = line[:i] } return strings.TrimSpace(line) } func readPIN(pin, pinFile string, pinStdin bool, stdin *bufio.Reader) (string, error) { switch { case pin != "": return pin, nil case pinFile != "": data, err := os.ReadFile(pinFile) if err != nil { return "", fmt.Errorf("read PIN file: %w", err) } defer memguard.WipeBytes(data) p := firstLine(data) if p == "" { return "", fmt.Errorf("PIN file is empty") } return p, nil case pinStdin: line, err := stdin.ReadString('\n') if err != nil && err != io.EOF { return "", fmt.Errorf("read PIN from stdin: %w", err) } p := strings.TrimRight(line, "\r\n") if p == "" { return "", fmt.Errorf("stdin PIN is empty") } return p, nil default: return "", fmt.Errorf("PIV PIN required; use --pin-stdin, --pin-file, or --pin") } } func logStatus(quiet bool, format string, args ...any) { if quiet { return } fmt.Fprintf(os.Stderr, format+"\n", args...) } func containsSignatureBlock(data []byte) bool { s := string(data) for algo := range supportedAlgorithms { if strings.Contains(s, "-----BEGIN "+algo+" SIGNATURE-----") { return true } } return false } func signData(data []byte, pin string) (string, error) { pinGuard := memguard.NewBufferFromBytes([]byte(pin)) defer pinGuard.Destroy() normalizedData := normalizeToRFCCompliantCRLF(data) sig, algo, err := signDataInternal(pinGuard.Bytes(), normalizedData) if err != nil { return "", fmt.Errorf("signing failed: %w", err) } sep := "\r\n" if len(normalizedData) > 0 { last := string(normalizedData[len(normalizedData)-1:]) if last == "\n" && !(len(normalizedData) >= 2 && string(normalizedData[len(normalizedData)-2:]) == "\r\n") { sep = "\n" } } return string(normalizedData) + sep + "-----BEGIN " + algo + " SIGNATURE-----" + sep + formatSignatureRFC(sig) + "-----END " + algo + " SIGNATURE-----" + sep, nil } func signDataInternal(pin, data []byte) (string, string, error) { yk, err := openYubiKey(yubiKeyIndex) if err != nil { return "", "", err } defer yk.Close() cert, err := yk.Certificate(piv.SlotSignature) if err != nil { return "", "", fmt.Errorf("failed to get certificate from signature slot: %w", err) } if ed25519PubKey, ok := cert.PublicKey.(ed25519.PublicKey); ok { hash := sha256.Sum256(data) return signEd25519Data(string(pin), hash[:], ed25519PubKey, yk) } pubKey, ok := cert.PublicKey.(*ecdsa.PublicKey) if !ok { return "", "", fmt.Errorf("public key is not ECDSA or Ed25519") } algorithm, exists := curveToAlgorithm[pubKey.Curve] if !exists { return "", "", fmt.Errorf("unsupported curve: %v", pubKey.Curve) } hashFunc := curveToHash[pubKey.Curve] var digest []byte switch hashFunc { case crypto.SHA256: h := sha256.New() h.Write(data) digest = h.Sum(nil) case crypto.SHA384: h := sha512.New384() h.Write(data) digest = h.Sum(nil) default: return "", "", fmt.Errorf("unsupported hash algorithm for curve") } auth := piv.KeyAuth{PIN: string(pin)} priv, err := yk.PrivateKey(piv.SlotSignature, cert.PublicKey, auth) if err != nil { return "", "", fmt.Errorf("failed to get private key: %w", err) } signer, ok := priv.(crypto.Signer) if !ok { return "", "", fmt.Errorf("key does not implement crypto.Signer") } asn1sig, err := signer.Sign(rand.Reader, digest, nil) if err != nil { return "", "", fmt.Errorf("signing failed: %w", err) } var sig ecSignature if _, err := asn1.Unmarshal(asn1sig, &sig); err != nil { return "", "", fmt.Errorf("ASN.1 unmarshal failed: %w", err) } curveSize := (pubKey.Curve.Params().BitSize + 7) / 8 var raw []byte raw = append(raw, safePad(pubKey.X.Bytes(), curveSize)...) raw = append(raw, safePad(pubKey.Y.Bytes(), curveSize)...) raw = append(raw, safePad(sig.R.Bytes(), curveSize)...) raw = append(raw, safePad(sig.S.Bytes(), curveSize)...) return hex.EncodeToString(raw), algorithm, nil } func signEd25519Data(pin string, hash []byte, pubKey ed25519.PublicKey, yk *piv.YubiKey) (string, string, error) { auth := piv.KeyAuth{PIN: pin} priv, err := yk.PrivateKey(piv.SlotSignature, pubKey, auth) if err != nil { return "", "", fmt.Errorf("failed to get private key: %w", err) } signer, ok := priv.(crypto.Signer) if !ok { return "", "", fmt.Errorf("key does not implement crypto.Signer") } signature, err := signer.Sign(rand.Reader, hash, crypto.Hash(0)) if err != nil { return "", "", fmt.Errorf("Ed25519 signing failed: %w", err) } combined := append(pubKey, signature...) return hex.EncodeToString(combined), AlgorithmED25519, nil } func verifySignedData(data []byte) (*signatureInfo, error) { s := string(normalizeToRFCCompliantCRLF(data)) var algorithm string beginIndex := -1 beginMarker := "" for algo := range supportedAlgorithms { marker := "-----BEGIN " + algo + " SIGNATURE-----" if idx := strings.Index(s, marker); idx >= 0 { algorithm = algo beginIndex = idx beginMarker = marker break } } if algorithm == "" { return nil, fmt.Errorf("no supported signature block found") } endMarker := "-----END " + algorithm + " SIGNATURE-----" hexStart := beginIndex + len(beginMarker) endRel := strings.Index(s[hexStart:], endMarker) if endRel < 0 { return nil, fmt.Errorf("invalid signature block format") } original := []byte(s[:beginIndex]) original = bytesTrimOneLineEnding(original) hexPart := s[hexStart : hexStart+endRel] hexPart = regexp.MustCompile(`[\r\n\s\t]+`).ReplaceAllString(hexPart, "") combined, err := hex.DecodeString(hexPart) if err != nil { return nil, fmt.Errorf("hex decode failed: %w", err) } var verificationErr error switch algorithm { case AlgorithmED25519: hash := sha256.Sum256(original) verificationErr = verifyEd25519(hash[:], combined) case AlgorithmECCP256, AlgorithmECCP384: verificationErr = verifyECDSA(original, combined, algorithm) default: verificationErr = fmt.Errorf("unsupported algorithm: %s", algorithm) } if verificationErr != nil { return nil, verificationErr } publicKey, err := extractPublicKeyFromSignature(combined, algorithm) if err != nil { return nil, err } return &signatureInfo{ Algorithm: algorithm, OriginalMessage: original, PublicKey: publicKey, }, nil } func extractPublicKeyFromSignature(combined []byte, algorithm string) ([]byte, error) { switch algorithm { case AlgorithmED25519: if len(combined) != Ed25519CombinedSize { return nil, fmt.Errorf("invalid Ed25519 signature block") } return combined[:Ed25519PublicKeySize], nil case AlgorithmECCP256, AlgorithmECCP384: var curve elliptic.Curve switch algorithm { case AlgorithmECCP256: curve = elliptic.P256() case AlgorithmECCP384: curve = elliptic.P384() } curveSize := (curve.Params().BitSize + 7) / 8 expectedBytes := 4 * curveSize if len(combined) != expectedBytes { return nil, fmt.Errorf("invalid signature block size: expected %d, got %d", expectedBytes, len(combined)) } return combined[:2*curveSize], nil default: return nil, fmt.Errorf("unsupported algorithm: %s", algorithm) } } func verifyEd25519(dataHash, combined []byte) error { if len(combined) != Ed25519CombinedSize { return fmt.Errorf("invalid Ed25519 signature block") } publicKey := combined[:Ed25519PublicKeySize] signature := combined[Ed25519PublicKeySize:] if !ed25519.Verify(ed25519.PublicKey(publicKey), dataHash, signature) { return fmt.Errorf("Ed25519 signature verification failed") } return nil } func verifyECDSA(data, combined []byte, algorithm string) error { var curve elliptic.Curve var hashFunc crypto.Hash switch algorithm { case AlgorithmECCP256: curve = elliptic.P256() hashFunc = crypto.SHA256 case AlgorithmECCP384: curve = elliptic.P384() hashFunc = crypto.SHA384 default: return fmt.Errorf("unsupported ECDSA algorithm: %s", algorithm) } curveSize := (curve.Params().BitSize + 7) / 8 expectedBytes := 4 * curveSize if len(combined) != expectedBytes { return fmt.Errorf("invalid signature block size: expected %d, got %d", expectedBytes, len(combined)) } x := new(big.Int).SetBytes(safePad(combined[0:curveSize], curveSize)) y := new(big.Int).SetBytes(safePad(combined[curveSize:2*curveSize], curveSize)) r := new(big.Int).SetBytes(safePad(combined[2*curveSize:3*curveSize], curveSize)) s := new(big.Int).SetBytes(safePad(combined[3*curveSize:], curveSize)) if !curve.IsOnCurve(x, y) { return fmt.Errorf("public key point (X,Y) is not on the curve %s", curve.Params().Name) } pub := &ecdsa.PublicKey{Curve: curve, X: x, Y: y} var digest []byte switch hashFunc { case crypto.SHA256: h := sha256.New() h.Write(data) digest = h.Sum(nil) case crypto.SHA384: h := sha512.New384() h.Write(data) digest = h.Sum(nil) } if !ecdsa.Verify(pub, digest, r, s) { return fmt.Errorf("signature verification failed") } return nil } func encryptData(data []byte, pubKeyFile string) (string, error) { pubKey, err := loadRSAPublicKey(pubKeyFile) if err != nil { return "", fmt.Errorf("failed to load public key: %w", err) } aesKeyGuard := memguard.NewBuffer(32) defer aesKeyGuard.Destroy() if _, err := rand.Read(aesKeyGuard.Bytes()); err != nil { return "", fmt.Errorf("failed to generate AES key: %w", err) } encryptedKey, err := rsa.EncryptPKCS1v15(rand.Reader, pubKey, aesKeyGuard.Bytes()) if err != nil { return "", fmt.Errorf("RSA encryption failed: %w", err) } defer memguard.WipeBytes(encryptedKey) encryptedData, err := encryptAES(data, aesKeyGuard.Bytes()) if err != nil { return "", fmt.Errorf("AES encryption failed: %w", err) } defer memguard.WipeBytes(encryptedData) combined := append(encryptedKey, encryptedData...) defer memguard.WipeBytes(combined) return formatBase64RFC(base64.StdEncoding.EncodeToString(combined)), nil } func decryptData(data []byte, pin string) ([]byte, error) { pinGuard := memguard.NewBufferFromBytes([]byte(pin)) defer pinGuard.Destroy() combined, err := decodeBase64Clean(data) if err != nil { return nil, err } defer memguard.WipeBytes(combined) yk, err := openYubiKey(yubiKeyIndex) if err != nil { return nil, fmt.Errorf("failed to open YubiKey: %w", err) } defer yk.Close() cert, err := yk.Certificate(piv.SlotKeyManagement) if err != nil { return nil, fmt.Errorf("failed to get certificate from slot 9d: %w", err) } rsaPubKey, ok := cert.PublicKey.(*rsa.PublicKey) if !ok { return nil, fmt.Errorf("certificate does not contain RSA public key") } if err := checkRSASecurity(rsaPubKey, "on YubiKey"); err != nil { return nil, err } keySize := rsaPubKey.Size() if len(combined) < keySize { return nil, fmt.Errorf("ciphertext too short") } encryptedKey := combined[:keySize] encryptedData := combined[keySize:] auth := piv.KeyAuth{PIN: string(pinGuard.Bytes())} priv, err := yk.PrivateKey(piv.SlotKeyManagement, cert.PublicKey, auth) if err != nil { return nil, fmt.Errorf("failed to get private key: %w", err) } decrypter, ok := priv.(crypto.Decrypter) if !ok { return nil, fmt.Errorf("private key does not support decryption") } decryptedPayload, err := decrypter.Decrypt(rand.Reader, encryptedKey, nil) if err != nil { return nil, fmt.Errorf("RSA decryption failed: %w", err) } defer memguard.WipeBytes(decryptedPayload) if len(decryptedPayload) != 32 { return nil, fmt.Errorf("invalid AES key size") } decryptedData, err := decryptAES(encryptedData, decryptedPayload) if err != nil { return nil, fmt.Errorf("AES decryption failed: %w", err) } return decryptedData, nil } func checkRSASecurity(pubKey *rsa.PublicKey, context string) error { keySize := pubKey.N.BitLen() if keySize < minRSABits { return fmt.Errorf("insecure %d-bit RSA key %s - minimum is %d-bit", keySize, context, minRSABits) } if _, supported := supportedRSASizes[keySize]; !supported { fmt.Fprintf(os.Stderr, "WARNING: %d-bit RSA key %s - supported sizes are 2048, 3072, 4096 bits\n", keySize, context) } if keySize == 1024 { fmt.Fprintf(os.Stderr, "CRITICAL WARNING: 1024-bit RSA keys %s are insecure and should not be used!\n", context) } return nil } func loadRSAPublicKey(filename string) (*rsa.PublicKey, error) { data, err := os.ReadFile(filename) if err != nil { return nil, fmt.Errorf("failed to read public key file: %w", err) } defer memguard.WipeBytes(data) block, _ := pem.Decode(data) if block == nil { return nil, fmt.Errorf("no data found in file") } switch block.Type { case "CERTIFICATE": cert, err := x509.ParseCertificate(block.Bytes) if err != nil { return nil, fmt.Errorf("failed to parse certificate: %w", err) } pubKey, ok := cert.PublicKey.(*rsa.PublicKey) if !ok { return nil, fmt.Errorf("certificate does not contain RSA public key") } if err := checkRSASecurity(pubKey, "in certificate "+filename); err != nil { return nil, err } return pubKey, nil case "PUBLIC KEY": pubInterface, err := x509.ParsePKIXPublicKey(block.Bytes) if err != nil { return nil, fmt.Errorf("failed to parse public key: %w", err) } pubKey, ok := pubInterface.(*rsa.PublicKey) if !ok { return nil, fmt.Errorf("not an RSA public key") } if err := checkRSASecurity(pubKey, "in file "+filename); err != nil { return nil, err } return pubKey, nil case "RSA PUBLIC KEY": pubKey, err := x509.ParsePKCS1PublicKey(block.Bytes) if err != nil { return nil, fmt.Errorf("failed to parse RSA public key: %w", err) } if err := checkRSASecurity(pubKey, "in file "+filename); err != nil { return nil, err } return pubKey, nil default: return nil, fmt.Errorf("unsupported type: %s, expected CERTIFICATE, PUBLIC KEY or RSA PUBLIC KEY", block.Type) } } func encryptAES(data, key []byte) ([]byte, error) { block, err := aes.NewCipher(key) if err != nil { return nil, err } gcm, err := cipher.NewGCM(block) if err != nil { return nil, err } nonce := make([]byte, gcm.NonceSize()) if _, err := rand.Read(nonce); err != nil { return nil, err } return gcm.Seal(nonce, nonce, data, nil), nil } func decryptAES(data, key []byte) ([]byte, error) { block, err := aes.NewCipher(key) if err != nil { return nil, err } gcm, err := cipher.NewGCM(block) if err != nil { return nil, err } nonceSize := gcm.NonceSize() if len(data) < nonceSize { return nil, fmt.Errorf("ciphertext too short") } nonce, ciphertext := data[:nonceSize], data[nonceSize:] plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) if err != nil { return nil, err } return plaintext, nil } func openYubiKey(index int) (*piv.YubiKey, error) { cards, err := piv.Cards() if err != nil { return nil, fmt.Errorf("failed to list cards: %w", err) } if len(cards) == 0 { return nil, fmt.Errorf("no smart card found") } count := 0 for _, card := range cards { if strings.Contains(strings.ToLower(card), "yubikey") { if count == index { return piv.Open(card) } count++ } } return nil, fmt.Errorf("no YubiKey found at index %d", index) } func normalizeToRFCCompliantCRLF(data []byte) []byte { s := string(data) s = strings.ReplaceAll(s, "\r\n", "\n") s = strings.ReplaceAll(s, "\r", "\n") s = strings.ReplaceAll(s, "\n", "\r\n") return []byte(s) } func formatSignatureRFC(sig string) string { var result strings.Builder for i := 0; i < len(sig); i += 64 { end := i + 64 if end > len(sig) { end = len(sig) } result.WriteString(sig[i:end]) result.WriteString("\r\n") } return result.String() } func formatBase64RFC(data string) string { var result strings.Builder for i := 0; i < len(data); i += 76 { end := i + 76 if end > len(data) { end = len(data) } result.WriteString(data[i:end]) result.WriteString("\r\n") } return result.String() } func securePadMessage(data []byte) []byte { const blockSize = 4096 paddingNeeded := blockSize - (len(data) % blockSize) if paddingNeeded == blockSize { return data } paddedData := make([]byte, len(data)+paddingNeeded) copy(paddedData, data) paddedData[len(data)] = 0x80 return paddedData } func secureUnpadMessage(data []byte) ([]byte, error) { if len(data) == 0 { return nil, errors.New("cannot unpad empty data") } if len(data)%4096 != 0 { return nil, errors.New("invalid block size for unpadding") } lastIndex := -1 for i := len(data) - 1; i >= 0; i-- { if data[i] == 0x80 { lastIndex = i break } if data[i] != 0x00 { return nil, errors.New("invalid padding format: unexpected non-zero byte") } } if lastIndex == -1 { return nil, errors.New("no padding marker found") } return data[:lastIndex], nil } func safePad(b []byte, size int) []byte { if len(b) > size { return b[len(b)-size:] } return append(make([]byte, size-len(b)), b...) } func bytesTrimOneLineEnding(data []byte) []byte { if len(data) >= 2 && data[len(data)-2] == '\r' && data[len(data)-1] == '\n' { return data[:len(data)-2] } if len(data) >= 1 && data[len(data)-1] == '\n' { return data[:len(data)-1] } return data } func decodeBase64Clean(data []byte) ([]byte, error) { s := string(data) s = strings.ReplaceAll(s, "\r", "") s = strings.ReplaceAll(s, "\n", "") s = strings.ReplaceAll(s, " ", "") decoded, err := base64.StdEncoding.DecodeString(s) if err != nil { return nil, fmt.Errorf("base64 decode failed: %w", err) } return decoded, nil }