Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 161 additions & 4 deletions libguile-ssh/key-func.c
Original file line number Diff line number Diff line change
Expand Up @@ -384,16 +384,17 @@ Read public key from a file FILENAME. Return a SSH key.\
#undef FUNC_NAME

static gssh_symbol_t hash_types[] = {
{ "sha1", SSH_PUBLICKEY_HASH_SHA1 },
{ "md5", SSH_PUBLICKEY_HASH_MD5 },
{ NULL, -1 }
{ "sha1", SSH_PUBLICKEY_HASH_SHA1 },
{ "sha256", SSH_PUBLICKEY_HASH_SHA256 },
{ "md5", SSH_PUBLICKEY_HASH_MD5 },
{ NULL, -1 }
};

SCM_DEFINE (guile_ssh_get_public_key_hash, "get-public-key-hash", 2, 0, 0,
(SCM key, SCM type),
"\
Get hash of the public KEY as a bytevector.\n\
Possible types are: 'sha1, 'md5\n\
Possible types are: 'sha1, 'sha256, 'md5\n\
Return a bytevector on success, #f on error.\
")
#define FUNC_NAME s_guile_ssh_get_public_key_hash
Expand Down Expand Up @@ -434,6 +435,162 @@ Return a bytevector on success, #f on error.\
}
#undef FUNC_NAME

SCM_DEFINE (guile_ssh_get_public_key_fingerprint, "%get-public-key-fingerprint", 2, 0, 0,
(SCM key, SCM type),
"\
Get fingerprint of the public KEY as a formatted string.\n\
Possible types are: 'sha1, 'sha256, 'md5\n\
Return a fingerprint string on success, #f on error.\
")
#define FUNC_NAME s_guile_ssh_get_public_key_fingerprint
{
gssh_key_t *kd = gssh_key_from_scm (key);
unsigned char *hash = NULL;
size_t hash_len;
char *fingerprint = NULL;
int res;
SCM ret;
const gssh_symbol_t *hash_type = NULL;

SCM_ASSERT (scm_is_symbol (type), type, SCM_ARG2, FUNC_NAME);

scm_dynwind_begin (0);

hash_type = gssh_symbol_from_scm (hash_types, type);
if (! hash_type)
guile_ssh_error1 (FUNC_NAME, "Wrong type", type);

res = ssh_get_publickey_hash (kd->ssh_key, hash_type->value,
&hash, &hash_len);
scm_dynwind_free (hash);

if (res == SSH_OK)
{
fingerprint = ssh_get_fingerprint_hash (hash_type->value, hash, hash_len);
if (fingerprint)
{
ret = scm_take_locale_string (fingerprint);
}
else
{
ret = SCM_BOOL_F;
}
}
else
{
ret = SCM_BOOL_F;
}

scm_dynwind_end ();
return ret;
}
#undef FUNC_NAME

static gssh_symbol_t sshsig_hash_types[] = {
{ "sha256", SSHSIG_DIGEST_SHA2_256 },
{ "sha512", SSHSIG_DIGEST_SHA2_512 },
{ NULL, -1 }
};

SCM_DEFINE (guile_ssh_sign, "%sshsig-sign", 4, 0, 0,
(SCM data_bv, SCM private_key, SCM sig_namespace, SCM hash_alg),
"\
Sign binary DATA_BV (bytevector) using a PRIVATE_KEY with specified SIG_NAMESPACE and HASH_ALG.\n\
HASH_ALG should be 'sha256 or 'sha512.\n\
Return the armored signature string on success, #f on error.\
")
#define FUNC_NAME s_guile_ssh_sign
{
gssh_key_t *kd = gssh_key_from_scm (private_key);
char *c_sig_namespace = NULL;
char *armored_sig = NULL;
const void *data;
size_t data_len;
const gssh_symbol_t *hash_type = NULL;
int res;
SCM ret;

SCM_ASSERT (scm_is_bytevector (data_bv), data_bv, SCM_ARG1, FUNC_NAME);
SCM_ASSERT (_private_key_p (kd), private_key, SCM_ARG2, FUNC_NAME);
SCM_ASSERT (scm_is_string (sig_namespace), sig_namespace, SCM_ARG3, FUNC_NAME);
SCM_ASSERT (scm_is_symbol (hash_alg), hash_alg, SCM_ARG4, FUNC_NAME);

scm_dynwind_begin (0);

data_len = scm_c_bytevector_length (data_bv);
data = SCM_BYTEVECTOR_CONTENTS (data_bv);

c_sig_namespace = scm_to_locale_string (sig_namespace);
scm_dynwind_free (c_sig_namespace);

hash_type = gssh_symbol_from_scm (sshsig_hash_types, hash_alg);
if (! hash_type)
guile_ssh_error1 (FUNC_NAME, "Wrong hash type", hash_alg);

res = sshsig_sign (data, data_len, kd->ssh_key, c_sig_namespace,
hash_type->value, &armored_sig);

if (res == SSH_OK)
{
ret = scm_take_locale_string (armored_sig);
}
else
{
ret = SCM_BOOL_F;
}

scm_dynwind_end ();
return ret;
}
#undef FUNC_NAME

SCM_DEFINE (guile_ssh_verify, "%sshsig-verify", 3, 0, 0,
(SCM data_bv, SCM signature, SCM sig_namespace),
"\
Verify a SIGNATURE for binary DATA_BV (bytevector) with SIG_NAMESPACE.\n\
Return the signing key on success, #f on error.\
")
#define FUNC_NAME s_guile_ssh_verify
{
char *c_signature = NULL;
char *c_sig_namespace = NULL;
const void *data;
size_t data_len;
ssh_key sign_key = NULL;
int res;
SCM ret;

SCM_ASSERT (scm_is_bytevector (data_bv), data_bv, SCM_ARG1, FUNC_NAME);
SCM_ASSERT (scm_is_string (signature), signature, SCM_ARG2, FUNC_NAME);
SCM_ASSERT (scm_is_string (sig_namespace), sig_namespace, SCM_ARG3, FUNC_NAME);

scm_dynwind_begin (0);

data_len = scm_c_bytevector_length (data_bv);
data = SCM_BYTEVECTOR_CONTENTS (data_bv);

c_signature = scm_to_locale_string (signature);
scm_dynwind_free (c_signature);

c_sig_namespace = scm_to_locale_string (sig_namespace);
scm_dynwind_free (c_sig_namespace);

res = sshsig_verify (data, data_len, c_signature, c_sig_namespace, &sign_key);

if (res == SSH_OK)
{
ret = gssh_key_to_scm (sign_key, SCM_BOOL_F);
}
else
{
ret = SCM_BOOL_F;
}

scm_dynwind_end ();
return ret;
}
#undef FUNC_NAME


/* Initialize Scheme procedures. */
void
Expand Down
3 changes: 3 additions & 0 deletions libguile-ssh/key-func.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ extern SCM guile_ssh_string_to_public_key (SCM arg1, SCM arg2);
extern SCM guile_ssh_public_key_to_string (SCM arg1);
extern SCM guile_ssh_private_key_from_file (SCM arg1, SCM arg2);
extern SCM guile_ssh_public_key_from_file (SCM arg1, SCM arg2);
extern SCM guile_ssh_get_public_key_fingerprint (SCM arg1, SCM arg2);
extern SCM guile_ssh_sign (SCM arg1, SCM arg2, SCM arg3, SCM arg4);
extern SCM guile_ssh_verify (SCM arg1, SCM arg2, SCM arg3);

extern void init_key_func (void);

Expand Down
34 changes: 33 additions & 1 deletion modules/ssh/key.scm
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,17 @@
;; private-key-from-file
;; private-key-to-file
;; get-public-key-hash
;; get-public-key-fingerprint
;; bytevector->hex-string
;; sign
;; verify


;;; Code:

(define-module (ssh key)
#:use-module (ice-9 format)
#:use-module (ice-9 match)
#:use-module (rnrs bytevectors)
#:use-module (ssh log)
#:export (key
Expand All @@ -57,8 +61,11 @@
private-key->public-key
private-key-from-file
private-key-to-file
get-public-key-fingerprint
get-public-key-hash
bytevector->hex-string))
bytevector->hex-string
sign
verify))

(define (bytevector->hex-string bv)
"Convert bytevector BV to a colon separated hex string."
Expand All @@ -72,6 +79,31 @@
(user-data #f))
(%private-key-from-file path auth-callback user-data))

(define* (get-public-key-fingerprint key #:optional (hash 'sha256))
(%get-public-key-fingerprint key hash))

(define* (sign data key
#:key
(namespace "file")
(hash 'sha512))
(match data
((? string?)
(%sshsig-sign (string->utf8 data) key namespace hash))
((? bytevector?)
(%sshsig-sign data key namespace hash))
(_
(error "sign: DATA must be a string or bytevector"))))

(define* (verify data signature
#:key (namespace "file"))
(match data
((? string?)
(%sshsig-verify (string->utf8 data) signature namespace))
((? bytevector?)
(%sshsig-sign data key namespace hash))
(_
(error "verify: DATA must be a string or bytevector"))))

(unless (getenv "GUILE_SSH_CROSS_COMPILING")
(load-extension "libguile-ssh" "init_key"))

Expand Down
107 changes: 107 additions & 0 deletions tests/key.scm
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,24 @@
(or (eq? (get-key-type key) 'ecdsa) ; libssh < 0.9
(eq? (get-key-type key) 'ecdsa-p256)))))


;;; Key fingerprints.

(test-assert-with-log "get-public-key-fingerprint: RSA SHA1"
(let ((fingerprint (get-public-key-fingerprint *rsa-pub-key* 'sha1)))
(and (string? fingerprint)
(> (string-length fingerprint) 0))))

(test-assert-with-log "get-public-key-fingerprint: RSA SHA256"
(let ((fingerprint (get-public-key-fingerprint *rsa-pub-key* 'sha256)))
(and (string? fingerprint)
(> (string-length fingerprint) 0))))

(test-assert-with-log "get-public-key-fingerprint: RSA MD5"
(let ((fingerprint (get-public-key-fingerprint *rsa-pub-key* 'md5)))
(and (string? fingerprint)
(> (string-length fingerprint) 0))))


;;; Check reading encrypted keys.

Expand Down Expand Up @@ -287,6 +305,95 @@
#:auth-callback (lambda (prompt max-len echo? verify? userdata)
"123")))


;;; Sign & Verify

(define %test-data "Hello, Guile-SSH world!")

(test-assert-with-log "sign: RSA"
(let* ((private-key (private-key-from-file %rsakey))
(signature (sign %test-data private-key)))
(and (string? signature)
(not (string-null? signature)))))

(test-assert-with-log "verify: RSA, valid signature"
(let* ((private-key (private-key-from-file %rsakey))
(signature (sign %test-data private-key))
(public-key (private-key->public-key private-key)))
(verify %test-data signature)))

(test-equal "verify: RSA, invalid signature"
#f
(let* ((private-key (private-key-from-file %rsakey))
(public-key (private-key->public-key private-key))
(fake-signature "invalid-signature"))
(catch #t
(lambda ()
(verify %test-data fake-signature))
(lambda args #f))))

(test-assert-with-log "sign with custom namespace and hash"
(let* ((private-key (private-key-from-file %rsakey))
(signature (sign %test-data private-key
#:namespace "test"
#:hash 'sha256)))
(and (string? signature)
(not (string-null? signature)))))

(test-assert-with-log "verify with custom namespace"
(let* ((private-key (private-key-from-file %rsakey))
(signature (sign %test-data private-key #:namespace "test"))
(public-key (private-key->public-key private-key)))
(verify %test-data signature #:namespace "test")))

(test-equal "verify: namespace mismatch"
#f
(let* ((private-key (private-key-from-file %rsakey))
(signature (sign %test-data private-key #:namespace "test"))
(public-key (private-key->public-key private-key)))
(catch #t
(lambda ()
(verify %test-data signature #:namespace "different"))
(lambda args #f))))

(unless-dsa-supported
(test-skip "sign: DSA"))
(test-assert-with-log "sign: DSA"
(let* ((private-key (private-key-from-file %dsakey))
(signature (sign %test-data private-key)))
(and (string? signature)
(not (string-null? signature)))))

(unless-dsa-supported
(test-skip "verify: DSA"))
(test-assert-with-log "verify: DSA"
(let* ((private-key (private-key-from-file %dsakey))
(signature (sign %test-data private-key))
(public-key (private-key->public-key private-key)))
(verify %test-data signature)))

(unless-openssl
(test-skip "sign: ECDSA"))
(test-assert-with-log "sign: ECDSA"
(let* ((private-key (private-key-from-file %ecdsakey))
(signature (sign %test-data private-key)))
(and (string? signature)
(not (string-null? signature)))))

(unless-openssl
(test-skip "verify: ECDSA"))
(test-assert-with-log "verify: ECDSA"
(let* ((private-key (private-key-from-file %ecdsakey))
(signature (sign %test-data private-key))
(public-key (private-key->public-key private-key)))
(verify %test-data signature)))

(test-error-with-log "sign: invalid key type"
(sign %test-data "not-a-key"))

(test-assert-with-log "verify: invalid signature format"
(not (verify %test-data "not-a-signature")))

;;;
(define exit-status (test-runner-fail-count (test-runner-current)))

Expand Down