Page Menu
Home
GnuPG
Search
Configure Global Search
Log In
Files
F23642718
command-ssh.c
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Size
93 KB
Subscribers
None
command-ssh.c
View Options
/* command-ssh.c - gpg-agent's implementation of the ssh-agent protocol.
* Copyright (C) 2004-2006, 2009, 2012 Free Software Foundation, Inc.
* Copyright (C) 2004-2006, 2009, 2012-2014 Werner Koch
*
* This file is part of GnuPG.
*
* GnuPG is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* GnuPG is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, see <https://www.gnu.org/licenses/>.
*/
/* Only v2 of the ssh-agent protocol is implemented. Relevant RFCs
are:
RFC-4250 - Protocol Assigned Numbers
RFC-4251 - Protocol Architecture
RFC-4252 - Authentication Protocol
RFC-4253 - Transport Layer Protocol
RFC-5656 - ECC support
The protocol for the agent is defined in OpenSSH's PROTOCL.agent
file.
*/
#include
<config.h>
#include
<stdio.h>
#include
<stdlib.h>
#include
<string.h>
#include
<errno.h>
#include
<sys/types.h>
#include
<sys/stat.h>
#include
<assert.h>
#ifndef HAVE_W32_SYSTEM
#include
<sys/socket.h>
#include
<sys/un.h>
#endif
/*!HAVE_W32_SYSTEM*/
#ifdef HAVE_UCRED_H
#include
<ucred.h>
#endif
#include
"agent.h"
#include
"../common/i18n.h"
#include
"../common/util.h"
#include
"../common/ssh-utils.h"
/* Request types. */
#define SSH_REQUEST_REQUEST_IDENTITIES 11
#define SSH_REQUEST_SIGN_REQUEST 13
#define SSH_REQUEST_ADD_IDENTITY 17
#define SSH_REQUEST_REMOVE_IDENTITY 18
#define SSH_REQUEST_REMOVE_ALL_IDENTITIES 19
#define SSH_REQUEST_LOCK 22
#define SSH_REQUEST_UNLOCK 23
#define SSH_REQUEST_ADD_ID_CONSTRAINED 25
/* Options. */
#define SSH_OPT_CONSTRAIN_LIFETIME 1
#define SSH_OPT_CONSTRAIN_CONFIRM 2
/* Response types. */
#define SSH_RESPONSE_SUCCESS 6
#define SSH_RESPONSE_FAILURE 5
#define SSH_RESPONSE_IDENTITIES_ANSWER 12
#define SSH_RESPONSE_SIGN_RESPONSE 14
/* Other constants. */
#define SSH_DSA_SIGNATURE_PADDING 20
#define SSH_DSA_SIGNATURE_ELEMS 2
#define SPEC_FLAG_USE_PKCS1V2 (1 << 0)
#define SPEC_FLAG_IS_ECDSA (1 << 1)
#define SPEC_FLAG_IS_EdDSA (1 << 2)
/*(lowercase 'd' on purpose.)*/
#define SPEC_FLAG_WITH_CERT (1 << 7)
/* The name of the control file. */
#define SSH_CONTROL_FILE_NAME "sshcontrol"
/* The blurb we put into the header of a newly created control file. */
static
const
char
sshcontrolblurb
[]
=
"# List of allowed ssh keys. Only keys present in this file are used
\n
"
"# in the SSH protocol. The ssh-add tool may add new entries to this
\n
"
"# file to enable them; you may also add them manually. Comment
\n
"
"# lines, like this one, as well as empty lines are ignored. Lines do
\n
"
"# have a certain length limit but this is not serious limitation as
\n
"
"# the format of the entries is fixed and checked by gpg-agent. A
\n
"
"# non-comment line starts with optional white spaces, followed by the
\n
"
"# keygrip of the key given as 40 hex digits, optionally followed by a
\n
"
"# caching TTL in seconds, and another optional field for arbitrary
\n
"
"# flags. Prepend the keygrip with an '!' mark to disable it.
\n
"
"
\n
"
;
/* Macros. */
/* Return a new uint32 with b0 being the most significant byte and b3
being the least significant byte. */
#define uint32_construct(b0, b1, b2, b3) \
((b0 << 24) | (b1 << 16) | (b2 << 8) | b3)
/*
* Basic types.
*/
/* Type for a request handler. */
typedef
gpg_error_t
(
*
ssh_request_handler_t
)
(
ctrl_t
ctrl
,
estream_t
request
,
estream_t
response
);
struct
ssh_key_type_spec
;
typedef
struct
ssh_key_type_spec
ssh_key_type_spec_t
;
/* Type, which is used for associating request handlers with the
appropriate request IDs. */
typedef
struct
ssh_request_spec
{
unsigned
char
type
;
ssh_request_handler_t
handler
;
const
char
*
identifier
;
unsigned
int
secret_input
;
}
ssh_request_spec_t
;
/* Type for "key modifier functions", which are necessary since
OpenSSH and GnuPG treat key material slightly different. A key
modifier is called right after a new key identity has been received
in order to "sanitize" the material. */
typedef
gpg_error_t
(
*
ssh_key_modifier_t
)
(
const
char
*
elems
,
gcry_mpi_t
*
mpis
);
/* The encoding of a generated signature is dependent on the
algorithm; therefore algorithm specific signature encoding
functions are necessary. */
typedef
gpg_error_t
(
*
ssh_signature_encoder_t
)
(
ssh_key_type_spec_t
*
spec
,
estream_t
signature_blob
,
gcry_sexp_t
sig
);
/* Type, which is used for boundling all the algorithm specific
information together in a single object. */
struct
ssh_key_type_spec
{
/* Algorithm identifier as used by OpenSSH. */
const
char
*
ssh_identifier
;
/* Human readable name of the algorithm. */
const
char
*
name
;
/* Algorithm identifier as used by GnuPG. */
int
algo
;
/* List of MPI names for secret keys; order matches the one of the
agent protocol. */
const
char
*
elems_key_secret
;
/* List of MPI names for public keys; order matches the one of the
agent protocol. */
const
char
*
elems_key_public
;
/* List of MPI names for signature data. */
const
char
*
elems_signature
;
/* List of MPI names for secret keys; order matches the one, which
is required by gpg-agent's key access layer. */
const
char
*
elems_sexp_order
;
/* Key modifier function. Key modifier functions are necessary in
order to fix any inconsistencies between the representation of
keys on the SSH and on the GnuPG side. */
ssh_key_modifier_t
key_modifier
;
/* Signature encoder function. Signature encoder functions are
necessary since the encoding of signatures depends on the used
algorithm. */
ssh_signature_encoder_t
signature_encoder
;
/* The name of the ECC curve or NULL. */
const
char
*
curve_name
;
/* The hash algorithm to be used with this key. 0 for using the
default. */
int
hash_algo
;
/* Misc flags. */
unsigned
int
flags
;
};
/* Definition of an object to access the sshcontrol file. */
struct
ssh_control_file_s
{
char
*
fname
;
/* Name of the file. */
FILE
*
fp
;
/* This is never NULL. */
int
lnr
;
/* The current line number. */
struct
{
int
valid
;
/* True if the data of this structure is valid. */
int
disabled
;
/* The item is disabled. */
int
ttl
;
/* The TTL of the item. */
int
confirm
;
/* The confirm flag is set. */
char
hexgrip
[
40
+
1
];
/* The hexgrip of the item (uppercase). */
}
item
;
};
/* Prototypes. */
static
gpg_error_t
ssh_handler_request_identities
(
ctrl_t
ctrl
,
estream_t
request
,
estream_t
response
);
static
gpg_error_t
ssh_handler_sign_request
(
ctrl_t
ctrl
,
estream_t
request
,
estream_t
response
);
static
gpg_error_t
ssh_handler_add_identity
(
ctrl_t
ctrl
,
estream_t
request
,
estream_t
response
);
static
gpg_error_t
ssh_handler_remove_identity
(
ctrl_t
ctrl
,
estream_t
request
,
estream_t
response
);
static
gpg_error_t
ssh_handler_remove_all_identities
(
ctrl_t
ctrl
,
estream_t
request
,
estream_t
response
);
static
gpg_error_t
ssh_handler_lock
(
ctrl_t
ctrl
,
estream_t
request
,
estream_t
response
);
static
gpg_error_t
ssh_handler_unlock
(
ctrl_t
ctrl
,
estream_t
request
,
estream_t
response
);
static
gpg_error_t
ssh_key_modifier_rsa
(
const
char
*
elems
,
gcry_mpi_t
*
mpis
);
static
gpg_error_t
ssh_signature_encoder_rsa
(
ssh_key_type_spec_t
*
spec
,
estream_t
signature_blob
,
gcry_sexp_t
signature
);
static
gpg_error_t
ssh_signature_encoder_dsa
(
ssh_key_type_spec_t
*
spec
,
estream_t
signature_blob
,
gcry_sexp_t
signature
);
static
gpg_error_t
ssh_signature_encoder_ecdsa
(
ssh_key_type_spec_t
*
spec
,
estream_t
signature_blob
,
gcry_sexp_t
signature
);
static
gpg_error_t
ssh_signature_encoder_eddsa
(
ssh_key_type_spec_t
*
spec
,
estream_t
signature_blob
,
gcry_sexp_t
signature
);
static
gpg_error_t
ssh_key_extract_comment
(
gcry_sexp_t
key
,
char
**
comment
);
/* Global variables. */
/* Associating request types with the corresponding request
handlers. */
static
ssh_request_spec_t
request_specs
[]
=
{
#define REQUEST_SPEC_DEFINE(id, name, secret_input) \
{ SSH_REQUEST_##id, ssh_handler_##name, #name, secret_input }
REQUEST_SPEC_DEFINE
(
REQUEST_IDENTITIES
,
request_identities
,
1
),
REQUEST_SPEC_DEFINE
(
SIGN_REQUEST
,
sign_request
,
0
),
REQUEST_SPEC_DEFINE
(
ADD_IDENTITY
,
add_identity
,
1
),
REQUEST_SPEC_DEFINE
(
ADD_ID_CONSTRAINED
,
add_identity
,
1
),
REQUEST_SPEC_DEFINE
(
REMOVE_IDENTITY
,
remove_identity
,
0
),
REQUEST_SPEC_DEFINE
(
REMOVE_ALL_IDENTITIES
,
remove_all_identities
,
0
),
REQUEST_SPEC_DEFINE
(
LOCK
,
lock
,
0
),
REQUEST_SPEC_DEFINE
(
UNLOCK
,
unlock
,
0
)
#undef REQUEST_SPEC_DEFINE
};
/* Table holding key type specifications. */
static
ssh_key_type_spec_t
ssh_key_types
[]
=
{
{
"ssh-ed25519"
,
"Ed25519"
,
GCRY_PK_EDDSA
,
"qd"
,
"q"
,
"rs"
,
"qd"
,
NULL
,
ssh_signature_encoder_eddsa
,
"Ed25519"
,
0
,
SPEC_FLAG_IS_EdDSA
},
{
"ssh-rsa"
,
"RSA"
,
GCRY_PK_RSA
,
"nedupq"
,
"en"
,
"s"
,
"nedpqu"
,
ssh_key_modifier_rsa
,
ssh_signature_encoder_rsa
,
NULL
,
0
,
SPEC_FLAG_USE_PKCS1V2
},
{
"ssh-dss"
,
"DSA"
,
GCRY_PK_DSA
,
"pqgyx"
,
"pqgy"
,
"rs"
,
"pqgyx"
,
NULL
,
ssh_signature_encoder_dsa
,
NULL
,
0
,
0
},
{
"ecdsa-sha2-nistp256"
,
"ECDSA"
,
GCRY_PK_ECC
,
"qd"
,
"q"
,
"rs"
,
"qd"
,
NULL
,
ssh_signature_encoder_ecdsa
,
"nistp256"
,
GCRY_MD_SHA256
,
SPEC_FLAG_IS_ECDSA
},
{
"ecdsa-sha2-nistp384"
,
"ECDSA"
,
GCRY_PK_ECC
,
"qd"
,
"q"
,
"rs"
,
"qd"
,
NULL
,
ssh_signature_encoder_ecdsa
,
"nistp384"
,
GCRY_MD_SHA384
,
SPEC_FLAG_IS_ECDSA
},
{
"ecdsa-sha2-nistp521"
,
"ECDSA"
,
GCRY_PK_ECC
,
"qd"
,
"q"
,
"rs"
,
"qd"
,
NULL
,
ssh_signature_encoder_ecdsa
,
"nistp521"
,
GCRY_MD_SHA512
,
SPEC_FLAG_IS_ECDSA
},
{
"ssh-ed25519-cert-v01@openssh.com"
,
"Ed25519"
,
GCRY_PK_EDDSA
,
"qd"
,
"q"
,
"rs"
,
"qd"
,
NULL
,
ssh_signature_encoder_eddsa
,
"Ed25519"
,
0
,
SPEC_FLAG_IS_EdDSA
|
SPEC_FLAG_WITH_CERT
},
{
"ssh-rsa-cert-v01@openssh.com"
,
"RSA"
,
GCRY_PK_RSA
,
"nedupq"
,
"en"
,
"s"
,
"nedpqu"
,
ssh_key_modifier_rsa
,
ssh_signature_encoder_rsa
,
NULL
,
0
,
SPEC_FLAG_USE_PKCS1V2
|
SPEC_FLAG_WITH_CERT
},
{
"ssh-dss-cert-v01@openssh.com"
,
"DSA"
,
GCRY_PK_DSA
,
"pqgyx"
,
"pqgy"
,
"rs"
,
"pqgyx"
,
NULL
,
ssh_signature_encoder_dsa
,
NULL
,
0
,
SPEC_FLAG_WITH_CERT
|
SPEC_FLAG_WITH_CERT
},
{
"ecdsa-sha2-nistp256-cert-v01@openssh.com"
,
"ECDSA"
,
GCRY_PK_ECC
,
"qd"
,
"q"
,
"rs"
,
"qd"
,
NULL
,
ssh_signature_encoder_ecdsa
,
"nistp256"
,
GCRY_MD_SHA256
,
SPEC_FLAG_IS_ECDSA
|
SPEC_FLAG_WITH_CERT
},
{
"ecdsa-sha2-nistp384-cert-v01@openssh.com"
,
"ECDSA"
,
GCRY_PK_ECC
,
"qd"
,
"q"
,
"rs"
,
"qd"
,
NULL
,
ssh_signature_encoder_ecdsa
,
"nistp384"
,
GCRY_MD_SHA384
,
SPEC_FLAG_IS_ECDSA
|
SPEC_FLAG_WITH_CERT
},
{
"ecdsa-sha2-nistp521-cert-v01@openssh.com"
,
"ECDSA"
,
GCRY_PK_ECC
,
"qd"
,
"q"
,
"rs"
,
"qd"
,
NULL
,
ssh_signature_encoder_ecdsa
,
"nistp521"
,
GCRY_MD_SHA512
,
SPEC_FLAG_IS_ECDSA
|
SPEC_FLAG_WITH_CERT
}
};
/*
General utility functions.
*/
/* A secure realloc, i.e. it makes sure to allocate secure memory if A
is NULL. This is required because the standard gcry_realloc does
not know whether to allocate secure or normal if NULL is passed as
existing buffer. */
static
void
*
realloc_secure
(
void
*
a
,
size_t
n
)
{
void
*
p
;
if
(
a
)
p
=
gcry_realloc
(
a
,
n
);
else
p
=
gcry_malloc_secure
(
n
);
return
p
;
}
/* Lookup the ssh-identifier for the ECC curve CURVE_NAME. Returns
NULL if not found. */
static
const
char
*
ssh_identifier_from_curve_name
(
const
char
*
curve_name
)
{
int
i
;
for
(
i
=
0
;
i
<
DIM
(
ssh_key_types
);
i
++
)
if
(
ssh_key_types
[
i
].
curve_name
&&
!
strcmp
(
ssh_key_types
[
i
].
curve_name
,
curve_name
))
return
ssh_key_types
[
i
].
ssh_identifier
;
return
NULL
;
}
/*
Primitive I/O functions.
*/
/* Read a byte from STREAM, store it in B. */
static
gpg_error_t
stream_read_byte
(
estream_t
stream
,
unsigned
char
*
b
)
{
gpg_error_t
err
;
int
ret
;
ret
=
es_fgetc
(
stream
);
if
(
ret
==
EOF
)
{
if
(
es_ferror
(
stream
))
err
=
gpg_error_from_syserror
();
else
err
=
gpg_error
(
GPG_ERR_EOF
);
*
b
=
0
;
}
else
{
*
b
=
ret
&
0xFF
;
err
=
0
;
}
return
err
;
}
/* Write the byte contained in B to STREAM. */
static
gpg_error_t
stream_write_byte
(
estream_t
stream
,
unsigned
char
b
)
{
gpg_error_t
err
;
int
ret
;
ret
=
es_fputc
(
b
,
stream
);
if
(
ret
==
EOF
)
err
=
gpg_error_from_syserror
();
else
err
=
0
;
return
err
;
}
/* Read a uint32 from STREAM, store it in UINT32. */
static
gpg_error_t
stream_read_uint32
(
estream_t
stream
,
u32
*
uint32
)
{
unsigned
char
buffer
[
4
];
size_t
bytes_read
;
gpg_error_t
err
;
int
ret
;
ret
=
es_read
(
stream
,
buffer
,
sizeof
(
buffer
),
&
bytes_read
);
if
(
ret
)
err
=
gpg_error_from_syserror
();
else
{
if
(
bytes_read
!=
sizeof
(
buffer
))
err
=
gpg_error
(
GPG_ERR_EOF
);
else
{
u32
n
;
n
=
uint32_construct
(
buffer
[
0
],
buffer
[
1
],
buffer
[
2
],
buffer
[
3
]);
*
uint32
=
n
;
err
=
0
;
}
}
return
err
;
}
/* Write the uint32 contained in UINT32 to STREAM. */
static
gpg_error_t
stream_write_uint32
(
estream_t
stream
,
u32
uint32
)
{
unsigned
char
buffer
[
4
];
gpg_error_t
err
;
int
ret
;
buffer
[
0
]
=
uint32
>>
24
;
buffer
[
1
]
=
uint32
>>
16
;
buffer
[
2
]
=
uint32
>>
8
;
buffer
[
3
]
=
uint32
>>
0
;
ret
=
es_write
(
stream
,
buffer
,
sizeof
(
buffer
),
NULL
);
if
(
ret
)
err
=
gpg_error_from_syserror
();
else
err
=
0
;
return
err
;
}
/* Read SIZE bytes from STREAM into BUFFER. */
static
gpg_error_t
stream_read_data
(
estream_t
stream
,
unsigned
char
*
buffer
,
size_t
size
)
{
gpg_error_t
err
;
size_t
bytes_read
;
int
ret
;
ret
=
es_read
(
stream
,
buffer
,
size
,
&
bytes_read
);
if
(
ret
)
err
=
gpg_error_from_syserror
();
else
{
if
(
bytes_read
!=
size
)
err
=
gpg_error
(
GPG_ERR_EOF
);
else
err
=
0
;
}
return
err
;
}
/* Skip over SIZE bytes from STREAM. */
static
gpg_error_t
stream_read_skip
(
estream_t
stream
,
size_t
size
)
{
char
buffer
[
128
];
size_t
bytes_to_read
,
bytes_read
;
int
ret
;
do
{
bytes_to_read
=
size
;
if
(
bytes_to_read
>
sizeof
buffer
)
bytes_to_read
=
sizeof
buffer
;
ret
=
es_read
(
stream
,
buffer
,
bytes_to_read
,
&
bytes_read
);
if
(
ret
)
return
gpg_error_from_syserror
();
else
if
(
bytes_read
!=
bytes_to_read
)
return
gpg_error
(
GPG_ERR_EOF
);
else
size
-=
bytes_to_read
;
}
while
(
size
);
return
0
;
}
/* Write SIZE bytes from BUFFER to STREAM. */
static
gpg_error_t
stream_write_data
(
estream_t
stream
,
const
unsigned
char
*
buffer
,
size_t
size
)
{
gpg_error_t
err
;
int
ret
;
ret
=
es_write
(
stream
,
buffer
,
size
,
NULL
);
if
(
ret
)
err
=
gpg_error_from_syserror
();
else
err
=
0
;
return
err
;
}
/* Read a binary string from STREAM into STRING, store size of string
in STRING_SIZE. Append a hidden nul so that the result may
directly be used as a C string. Depending on SECURE use secure
memory for STRING. If STRING is NULL do only a dummy read. */
static
gpg_error_t
stream_read_string
(
estream_t
stream
,
unsigned
int
secure
,
unsigned
char
**
string
,
u32
*
string_size
)
{
gpg_error_t
err
;
unsigned
char
*
buffer
=
NULL
;
u32
length
=
0
;
if
(
string_size
)
*
string_size
=
0
;
/* Read string length. */
err
=
stream_read_uint32
(
stream
,
&
length
);
if
(
err
)
goto
out
;
if
(
string
)
{
/* Allocate space. */
if
(
secure
)
buffer
=
xtrymalloc_secure
(
length
+
1
);
else
buffer
=
xtrymalloc
(
length
+
1
);
if
(
!
buffer
)
{
err
=
gpg_error_from_syserror
();
goto
out
;
}
/* Read data. */
err
=
stream_read_data
(
stream
,
buffer
,
length
);
if
(
err
)
goto
out
;
/* Finalize string object. */
buffer
[
length
]
=
0
;
*
string
=
buffer
;
}
else
/* Dummy read requested. */
{
err
=
stream_read_skip
(
stream
,
length
);
if
(
err
)
goto
out
;
}
if
(
string_size
)
*
string_size
=
length
;
out
:
if
(
err
)
xfree
(
buffer
);
return
err
;
}
/* Read a binary string from STREAM and store it as an opaque MPI at
R_MPI, adding 0x40 (this is the prefix for EdDSA key in OpenPGP).
Depending on SECURE use secure memory. If the string is too large
for key material return an error. */
static
gpg_error_t
stream_read_blob
(
estream_t
stream
,
unsigned
int
secure
,
gcry_mpi_t
*
r_mpi
)
{
gpg_error_t
err
;
unsigned
char
*
buffer
=
NULL
;
u32
length
=
0
;
*
r_mpi
=
NULL
;
/* Read string length. */
err
=
stream_read_uint32
(
stream
,
&
length
);
if
(
err
)
goto
leave
;
/* To avoid excessive use of secure memory we check that an MPI is
not too large. */
if
(
length
>
(
4096
/
8
)
+
8
)
{
log_error
(
_
(
"ssh keys greater than %d bits are not supported
\n
"
),
4096
);
err
=
GPG_ERR_TOO_LARGE
;
goto
leave
;
}
/* Allocate space. */
if
(
secure
)
buffer
=
xtrymalloc_secure
(
length
+
1
);
else
buffer
=
xtrymalloc
(
length
+
1
);
if
(
!
buffer
)
{
err
=
gpg_error_from_syserror
();
goto
leave
;
}
/* Read data. */
err
=
stream_read_data
(
stream
,
buffer
+
1
,
length
);
if
(
err
)
goto
leave
;
buffer
[
0
]
=
0x40
;
*
r_mpi
=
gcry_mpi_set_opaque
(
NULL
,
buffer
,
8
*
(
length
+
1
));
buffer
=
NULL
;
leave
:
xfree
(
buffer
);
return
err
;
}
/* Read a C-string from STREAM, store copy in STRING. */
static
gpg_error_t
stream_read_cstring
(
estream_t
stream
,
char
**
string
)
{
gpg_error_t
err
;
unsigned
char
*
buffer
;
err
=
stream_read_string
(
stream
,
0
,
&
buffer
,
NULL
);
if
(
!
err
)
*
string
=
(
char
*
)
buffer
;
return
err
;
}
/* Write a binary string from STRING of size STRING_N to STREAM. */
static
gpg_error_t
stream_write_string
(
estream_t
stream
,
const
unsigned
char
*
string
,
u32
string_n
)
{
gpg_error_t
err
;
err
=
stream_write_uint32
(
stream
,
string_n
);
if
(
err
)
goto
out
;
err
=
stream_write_data
(
stream
,
string
,
string_n
);
out
:
return
err
;
}
/* Write a C-string from STRING to STREAM. */
static
gpg_error_t
stream_write_cstring
(
estream_t
stream
,
const
char
*
string
)
{
gpg_error_t
err
;
err
=
stream_write_string
(
stream
,
(
const
unsigned
char
*
)
string
,
strlen
(
string
));
return
err
;
}
/* Read an MPI from STREAM, store it in MPINT. Depending on SECURE
use secure memory. */
static
gpg_error_t
stream_read_mpi
(
estream_t
stream
,
unsigned
int
secure
,
gcry_mpi_t
*
mpint
)
{
unsigned
char
*
mpi_data
;
u32
mpi_data_size
;
gpg_error_t
err
;
gcry_mpi_t
mpi
;
mpi_data
=
NULL
;
err
=
stream_read_string
(
stream
,
secure
,
&
mpi_data
,
&
mpi_data_size
);
if
(
err
)
goto
out
;
/* To avoid excessive use of secure memory we check that an MPI is
not too large. */
if
(
mpi_data_size
>
520
)
{
log_error
(
_
(
"ssh keys greater than %d bits are not supported
\n
"
),
4096
);
err
=
GPG_ERR_TOO_LARGE
;
goto
out
;
}
err
=
gcry_mpi_scan
(
&
mpi
,
GCRYMPI_FMT_STD
,
mpi_data
,
mpi_data_size
,
NULL
);
if
(
err
)
goto
out
;
*
mpint
=
mpi
;
out
:
xfree
(
mpi_data
);
return
err
;
}
/* Write the MPI contained in MPINT to STREAM. */
static
gpg_error_t
stream_write_mpi
(
estream_t
stream
,
gcry_mpi_t
mpint
)
{
unsigned
char
*
mpi_buffer
;
size_t
mpi_buffer_n
;
gpg_error_t
err
;
mpi_buffer
=
NULL
;
err
=
gcry_mpi_aprint
(
GCRYMPI_FMT_STD
,
&
mpi_buffer
,
&
mpi_buffer_n
,
mpint
);
if
(
err
)
goto
out
;
err
=
stream_write_string
(
stream
,
mpi_buffer
,
mpi_buffer_n
);
out
:
xfree
(
mpi_buffer
);
return
err
;
}
/* Copy data from SRC to DST until EOF is reached. */
static
gpg_error_t
stream_copy
(
estream_t
dst
,
estream_t
src
)
{
char
buffer
[
BUFSIZ
];
size_t
bytes_read
;
gpg_error_t
err
;
int
ret
;
err
=
0
;
while
(
1
)
{
ret
=
es_read
(
src
,
buffer
,
sizeof
(
buffer
),
&
bytes_read
);
if
(
ret
||
(
!
bytes_read
))
{
if
(
ret
)
err
=
gpg_error_from_syserror
();
break
;
}
ret
=
es_write
(
dst
,
buffer
,
bytes_read
,
NULL
);
if
(
ret
)
{
err
=
gpg_error_from_syserror
();
break
;
}
}
return
err
;
}
/* Open the ssh control file and create it if not available. With
APPEND passed as true the file will be opened in append mode,
otherwise in read only mode. On success 0 is returned and a new
control file object stored at R_CF. On error an error code is
returned and NULL is stored at R_CF. */
static
gpg_error_t
open_control_file
(
ssh_control_file_t
*
r_cf
,
int
append
)
{
gpg_error_t
err
;
ssh_control_file_t
cf
;
cf
=
xtrycalloc
(
1
,
sizeof
*
cf
);
if
(
!
cf
)
{
err
=
gpg_error_from_syserror
();
goto
leave
;
}
/* Note: As soon as we start to use non blocking functions here
(i.e. where Pth might switch threads) we need to employ a
mutex. */
cf
->
fname
=
make_filename_try
(
gnupg_homedir
(),
SSH_CONTROL_FILE_NAME
,
NULL
);
if
(
!
cf
->
fname
)
{
err
=
gpg_error_from_syserror
();
goto
leave
;
}
/* FIXME: With "a+" we are not able to check whether this will
be created and thus the blurb needs to be written first. */
cf
->
fp
=
fopen
(
cf
->
fname
,
append
?
"a+"
:
"r"
);
if
(
!
cf
->
fp
&&
errno
==
ENOENT
)
{
estream_t
stream
=
es_fopen
(
cf
->
fname
,
"wx,mode=-rw-r"
);
if
(
!
stream
)
{
err
=
gpg_error_from_syserror
();
log_error
(
_
(
"can't create '%s': %s
\n
"
),
cf
->
fname
,
gpg_strerror
(
err
));
goto
leave
;
}
es_fputs
(
sshcontrolblurb
,
stream
);
es_fclose
(
stream
);
cf
->
fp
=
fopen
(
cf
->
fname
,
append
?
"a+"
:
"r"
);
}
if
(
!
cf
->
fp
)
{
err
=
gpg_error_from_syserror
();
log_error
(
_
(
"can't open '%s': %s
\n
"
),
cf
->
fname
,
gpg_strerror
(
err
));
goto
leave
;
}
err
=
0
;
leave
:
if
(
err
&&
cf
)
{
if
(
cf
->
fp
)
fclose
(
cf
->
fp
);
xfree
(
cf
->
fname
);
xfree
(
cf
);
}
else
*
r_cf
=
cf
;
return
err
;
}
static
void
rewind_control_file
(
ssh_control_file_t
cf
)
{
fseek
(
cf
->
fp
,
0
,
SEEK_SET
);
cf
->
lnr
=
0
;
clearerr
(
cf
->
fp
);
}
static
void
close_control_file
(
ssh_control_file_t
cf
)
{
if
(
!
cf
)
return
;
fclose
(
cf
->
fp
);
xfree
(
cf
->
fname
);
xfree
(
cf
);
}
/* Read the next line from the control file and store the data in CF.
Returns 0 on success, GPG_ERR_EOF on EOF, or other error codes. */
static
gpg_error_t
read_control_file_item
(
ssh_control_file_t
cf
)
{
int
c
,
i
,
n
;
char
*
p
,
*
pend
,
line
[
256
];
long
ttl
=
0
;
cf
->
item
.
valid
=
0
;
clearerr
(
cf
->
fp
);
do
{
if
(
!
fgets
(
line
,
DIM
(
line
)
-1
,
cf
->
fp
)
)
{
if
(
feof
(
cf
->
fp
))
return
gpg_error
(
GPG_ERR_EOF
);
return
gpg_error_from_syserror
();
}
cf
->
lnr
++
;
if
(
!*
line
||
line
[
strlen
(
line
)
-1
]
!=
'\n'
)
{
/* Eat until end of line */
while
(
(
c
=
getc
(
cf
->
fp
))
!=
EOF
&&
c
!=
'\n'
)
;
return
gpg_error
(
*
line
?
GPG_ERR_LINE_TOO_LONG
:
GPG_ERR_INCOMPLETE_LINE
);
}
/* Allow for empty lines and spaces */
for
(
p
=
line
;
spacep
(
p
);
p
++
)
;
}
while
(
!*
p
||
*
p
==
'\n'
||
*
p
==
'#'
);
cf
->
item
.
disabled
=
0
;
if
(
*
p
==
'!'
)
{
cf
->
item
.
disabled
=
1
;
for
(
p
++
;
spacep
(
p
);
p
++
)
;
}
for
(
i
=
0
;
hexdigitp
(
p
)
&&
i
<
40
;
p
++
,
i
++
)
cf
->
item
.
hexgrip
[
i
]
=
(
*
p
>=
'a'
?
(
*
p
&
0xdf
)
:
*
p
);
cf
->
item
.
hexgrip
[
i
]
=
0
;
if
(
i
!=
40
||
!
(
spacep
(
p
)
||
*
p
==
'\n'
))
{
log_error
(
"%s:%d: invalid formatted line
\n
"
,
cf
->
fname
,
cf
->
lnr
);
return
gpg_error
(
GPG_ERR_BAD_DATA
);
}
ttl
=
strtol
(
p
,
&
pend
,
10
);
p
=
pend
;
if
(
!
(
spacep
(
p
)
||
*
p
==
'\n'
)
||
(
int
)
ttl
<
-1
)
{
log_error
(
"%s:%d: invalid TTL value; assuming 0
\n
"
,
cf
->
fname
,
cf
->
lnr
);
cf
->
item
.
ttl
=
0
;
}
cf
->
item
.
ttl
=
ttl
;
/* Now check for key-value pairs of the form NAME[=VALUE]. */
cf
->
item
.
confirm
=
0
;
while
(
*
p
)
{
for
(;
spacep
(
p
)
&&
*
p
!=
'\n'
;
p
++
)
;
if
(
!*
p
||
*
p
==
'\n'
)
break
;
n
=
strcspn
(
p
,
"=
\t\n
"
);
if
(
p
[
n
]
==
'='
)
{
log_error
(
"%s:%d: assigning a value to a flag is not yet supported; "
"flag ignored
\n
"
,
cf
->
fname
,
cf
->
lnr
);
p
++
;
}
else
if
(
n
==
7
&&
!
memcmp
(
p
,
"confirm"
,
7
))
{
cf
->
item
.
confirm
=
1
;
}
else
log_error
(
"%s:%d: invalid flag '%.*s'; ignored
\n
"
,
cf
->
fname
,
cf
->
lnr
,
n
,
p
);
p
+=
n
;
}
/* log_debug ("%s:%d: grip=%s ttl=%d%s%s\n", */
/* cf->fname, cf->lnr, */
/* cf->item.hexgrip, cf->item.ttl, */
/* cf->item.disabled? " disabled":"", */
/* cf->item.confirm? " confirm":""); */
cf
->
item
.
valid
=
1
;
return
0
;
/* Okay: valid entry found. */
}
/* Search the control file CF from the beginning until a matching
HEXGRIP is found; return success in this case and store true at
DISABLED if the found key has been disabled. If R_TTL is not NULL
a specified TTL for that key is stored there. If R_CONFIRM is not
NULL it is set to 1 if the key has the confirm flag set. */
static
gpg_error_t
search_control_file
(
ssh_control_file_t
cf
,
const
char
*
hexgrip
,
int
*
r_disabled
,
int
*
r_ttl
,
int
*
r_confirm
)
{
gpg_error_t
err
;
assert
(
strlen
(
hexgrip
)
==
40
);
if
(
r_disabled
)
*
r_disabled
=
0
;
if
(
r_ttl
)
*
r_ttl
=
0
;
if
(
r_confirm
)
*
r_confirm
=
0
;
rewind_control_file
(
cf
);
while
(
!
(
err
=
read_control_file_item
(
cf
)))
{
if
(
!
cf
->
item
.
valid
)
continue
;
/* Should not happen. */
if
(
!
strcmp
(
hexgrip
,
cf
->
item
.
hexgrip
))
break
;
}
if
(
!
err
)
{
if
(
r_disabled
)
*
r_disabled
=
cf
->
item
.
disabled
;
if
(
r_ttl
)
*
r_ttl
=
cf
->
item
.
ttl
;
if
(
r_confirm
)
*
r_confirm
=
cf
->
item
.
confirm
;
}
return
err
;
}
/* Add an entry to the control file to mark the key with the keygrip
HEXGRIP as usable for SSH; i.e. it will be returned when ssh asks
for it. FMTFPR is the fingerprint string. This function is in
general used to add a key received through the ssh-add function.
We can assume that the user wants to allow ssh using this key. */
static
gpg_error_t
add_control_entry
(
ctrl_t
ctrl
,
ssh_key_type_spec_t
*
spec
,
const
char
*
hexgrip
,
const
char
*
fmtfpr
,
int
ttl
,
int
confirm
)
{
gpg_error_t
err
;
ssh_control_file_t
cf
;
int
disabled
;
(
void
)
ctrl
;
err
=
open_control_file
(
&
cf
,
1
);
if
(
err
)
return
err
;
err
=
search_control_file
(
cf
,
hexgrip
,
&
disabled
,
NULL
,
NULL
);
if
(
err
&&
gpg_err_code
(
err
)
==
GPG_ERR_EOF
)
{
struct
tm
*
tp
;
time_t
atime
=
time
(
NULL
);
/* Not yet in the file - add it. Because the file has been
opened in append mode, we simply need to write to it. */
tp
=
localtime
(
&
atime
);
fprintf
(
cf
->
fp
,
(
"# %s key added on: %04d-%02d-%02d %02d:%02d:%02d
\n
"
"# MD5 Fingerprint: %s
\n
"
"%s %d%s
\n
"
),
spec
->
name
,
1900
+
tp
->
tm_year
,
tp
->
tm_mon
+
1
,
tp
->
tm_mday
,
tp
->
tm_hour
,
tp
->
tm_min
,
tp
->
tm_sec
,
fmtfpr
,
hexgrip
,
ttl
,
confirm
?
" confirm"
:
""
);
}
close_control_file
(
cf
);
return
0
;
}
/* Scan the sshcontrol file and return the TTL. */
static
int
ttl_from_sshcontrol
(
const
char
*
hexgrip
)
{
ssh_control_file_t
cf
;
int
disabled
,
ttl
;
if
(
!
hexgrip
||
strlen
(
hexgrip
)
!=
40
)
return
0
;
/* Wrong input: Use global default. */
if
(
open_control_file
(
&
cf
,
0
))
return
0
;
/* Error: Use the global default TTL. */
if
(
search_control_file
(
cf
,
hexgrip
,
&
disabled
,
&
ttl
,
NULL
)
||
disabled
)
ttl
=
0
;
/* Use the global default if not found or disabled. */
close_control_file
(
cf
);
return
ttl
;
}
/* Scan the sshcontrol file and return the confirm flag. */
static
int
confirm_flag_from_sshcontrol
(
const
char
*
hexgrip
)
{
ssh_control_file_t
cf
;
int
disabled
,
confirm
;
if
(
!
hexgrip
||
strlen
(
hexgrip
)
!=
40
)
return
1
;
/* Wrong input: Better ask for confirmation. */
if
(
open_control_file
(
&
cf
,
0
))
return
1
;
/* Error: Better ask for confirmation. */
if
(
search_control_file
(
cf
,
hexgrip
,
&
disabled
,
NULL
,
&
confirm
)
||
disabled
)
confirm
=
0
;
/* If not found or disabled, there is no reason to
ask for confirmation. */
close_control_file
(
cf
);
return
confirm
;
}
/* Open the ssh control file for reading. This is a public version of
open_control_file. The caller must use ssh_close_control_file to
release the returned handle. */
ssh_control_file_t
ssh_open_control_file
(
void
)
{
ssh_control_file_t
cf
;
/* Then look at all the registered and non-disabled keys. */
if
(
open_control_file
(
&
cf
,
0
))
return
NULL
;
return
cf
;
}
/* Close an ssh control file handle. This is the public version of
close_control_file. CF may be NULL. */
void
ssh_close_control_file
(
ssh_control_file_t
cf
)
{
close_control_file
(
cf
);
}
/* Read the next item from the ssh control file. The function returns
0 if a item was read, GPG_ERR_EOF on eof or another error value.
R_HEXGRIP shall either be null or a BUFFER of at least 41 byte.
R_DISABLED, R_TTLm and R_CONFIRM return flags from the control
file; they are only set on success. */
gpg_error_t
ssh_read_control_file
(
ssh_control_file_t
cf
,
char
*
r_hexgrip
,
int
*
r_disabled
,
int
*
r_ttl
,
int
*
r_confirm
)
{
gpg_error_t
err
;
do
err
=
read_control_file_item
(
cf
);
while
(
!
err
&&
!
cf
->
item
.
valid
);
if
(
!
err
)
{
if
(
r_hexgrip
)
strcpy
(
r_hexgrip
,
cf
->
item
.
hexgrip
);
if
(
r_disabled
)
*
r_disabled
=
cf
->
item
.
disabled
;
if
(
r_ttl
)
*
r_ttl
=
cf
->
item
.
ttl
;
if
(
r_confirm
)
*
r_confirm
=
cf
->
item
.
confirm
;
}
return
err
;
}
/* Search for a key with HEXGRIP in sshcontrol and return all
info. */
gpg_error_t
ssh_search_control_file
(
ssh_control_file_t
cf
,
const
char
*
hexgrip
,
int
*
r_disabled
,
int
*
r_ttl
,
int
*
r_confirm
)
{
gpg_error_t
err
;
int
i
;
const
char
*
s
;
char
uphexgrip
[
41
];
/* We need to make sure that HEXGRIP is all uppercase. The easiest
way to do this and also check its length is by copying to a
second buffer. */
for
(
i
=
0
,
s
=
hexgrip
;
i
<
40
&&
*
s
;
s
++
,
i
++
)
uphexgrip
[
i
]
=
*
s
>=
'a'
?
(
*
s
&
0xdf
)
:
*
s
;
uphexgrip
[
i
]
=
0
;
if
(
i
!=
40
)
err
=
gpg_error
(
GPG_ERR_INV_LENGTH
);
else
err
=
search_control_file
(
cf
,
uphexgrip
,
r_disabled
,
r_ttl
,
r_confirm
);
if
(
gpg_err_code
(
err
)
==
GPG_ERR_EOF
)
err
=
gpg_error
(
GPG_ERR_NOT_FOUND
);
return
err
;
}
/*
MPI lists.
*/
/* Free the list of MPIs MPI_LIST. */
static
void
mpint_list_free
(
gcry_mpi_t
*
mpi_list
)
{
if
(
mpi_list
)
{
unsigned
int
i
;
for
(
i
=
0
;
mpi_list
[
i
];
i
++
)
gcry_mpi_release
(
mpi_list
[
i
]);
xfree
(
mpi_list
);
}
}
/* Receive key material MPIs from STREAM according to KEY_SPEC;
depending on SECRET expect a public key or secret key. CERT is the
certificate blob used if KEY_SPEC indicates the certificate format;
it needs to be positioned to the end of the nonce. The newly
allocated list of MPIs is stored in MPI_LIST. Returns usual error
code. */
static
gpg_error_t
ssh_receive_mpint_list
(
estream_t
stream
,
int
secret
,
ssh_key_type_spec_t
*
spec
,
estream_t
cert
,
gcry_mpi_t
**
mpi_list
)
{
const
char
*
elems_public
;
unsigned
int
elems_n
;
const
char
*
elems
;
int
elem_is_secret
;
gcry_mpi_t
*
mpis
=
NULL
;
gpg_error_t
err
=
0
;
unsigned
int
i
;
if
(
secret
)
elems
=
spec
->
elems_key_secret
;
else
elems
=
spec
->
elems_key_public
;
elems_n
=
strlen
(
elems
);
elems_public
=
spec
->
elems_key_public
;
/* Check that either both, CERT and the WITH_CERT flag, are given or
none of them. */
if
(
!
(
!!
(
spec
->
flags
&
SPEC_FLAG_WITH_CERT
)
^
!
cert
))
{
err
=
gpg_error
(
GPG_ERR_INV_CERT_OBJ
);
goto
out
;
}
mpis
=
xtrycalloc
(
elems_n
+
1
,
sizeof
*
mpis
);
if
(
!
mpis
)
{
err
=
gpg_error_from_syserror
();
goto
out
;
}
elem_is_secret
=
0
;
for
(
i
=
0
;
i
<
elems_n
;
i
++
)
{
if
(
secret
)
elem_is_secret
=
!
strchr
(
elems_public
,
elems
[
i
]);
if
(
cert
&&
!
elem_is_secret
)
err
=
stream_read_mpi
(
cert
,
elem_is_secret
,
&
mpis
[
i
]);
else
err
=
stream_read_mpi
(
stream
,
elem_is_secret
,
&
mpis
[
i
]);
if
(
err
)
goto
out
;
}
*
mpi_list
=
mpis
;
mpis
=
NULL
;
out
:
if
(
err
)
mpint_list_free
(
mpis
);
return
err
;
}
/* Key modifier function for RSA. */
static
gpg_error_t
ssh_key_modifier_rsa
(
const
char
*
elems
,
gcry_mpi_t
*
mpis
)
{
gcry_mpi_t
p
;
gcry_mpi_t
q
;
gcry_mpi_t
u
;
if
(
strcmp
(
elems
,
"nedupq"
))
/* Modifying only necessary for secret keys. */
goto
out
;
u
=
mpis
[
3
];
p
=
mpis
[
4
];
q
=
mpis
[
5
];
if
(
gcry_mpi_cmp
(
p
,
q
)
>
0
)
{
/* P shall be smaller then Q! Swap primes. iqmp becomes u. */
gcry_mpi_t
tmp
;
tmp
=
mpis
[
4
];
mpis
[
4
]
=
mpis
[
5
];
mpis
[
5
]
=
tmp
;
}
else
/* U needs to be recomputed. */
gcry_mpi_invm
(
u
,
p
,
q
);
out
:
return
0
;
}
/* Signature encoder function for RSA. */
static
gpg_error_t
ssh_signature_encoder_rsa
(
ssh_key_type_spec_t
*
spec
,
estream_t
signature_blob
,
gcry_sexp_t
s_signature
)
{
gpg_error_t
err
=
0
;
gcry_sexp_t
valuelist
=
NULL
;
gcry_sexp_t
sublist
=
NULL
;
gcry_mpi_t
sig_value
=
NULL
;
gcry_mpi_t
*
mpis
=
NULL
;
const
char
*
elems
;
size_t
elems_n
;
int
i
;
unsigned
char
*
data
;
size_t
data_n
;
gcry_mpi_t
s
;
valuelist
=
gcry_sexp_nth
(
s_signature
,
1
);
if
(
!
valuelist
)
{
err
=
gpg_error
(
GPG_ERR_INV_SEXP
);
goto
out
;
}
elems
=
spec
->
elems_signature
;
elems_n
=
strlen
(
elems
);
mpis
=
xtrycalloc
(
elems_n
+
1
,
sizeof
*
mpis
);
if
(
!
mpis
)
{
err
=
gpg_error_from_syserror
();
goto
out
;
}
for
(
i
=
0
;
i
<
elems_n
;
i
++
)
{
sublist
=
gcry_sexp_find_token
(
valuelist
,
spec
->
elems_signature
+
i
,
1
);
if
(
!
sublist
)
{
err
=
gpg_error
(
GPG_ERR_INV_SEXP
);
break
;
}
sig_value
=
gcry_sexp_nth_mpi
(
sublist
,
1
,
GCRYMPI_FMT_USG
);
if
(
!
sig_value
)
{
err
=
gpg_error
(
GPG_ERR_INTERNAL
);
/* FIXME? */
break
;
}
gcry_sexp_release
(
sublist
);
sublist
=
NULL
;
mpis
[
i
]
=
sig_value
;
}
if
(
err
)
goto
out
;
/* RSA specific */
s
=
mpis
[
0
];
err
=
gcry_mpi_aprint
(
GCRYMPI_FMT_USG
,
&
data
,
&
data_n
,
s
);
if
(
err
)
goto
out
;
err
=
stream_write_string
(
signature_blob
,
data
,
data_n
);
xfree
(
data
);
out
:
gcry_sexp_release
(
valuelist
);
gcry_sexp_release
(
sublist
);
mpint_list_free
(
mpis
);
return
err
;
}
/* Signature encoder function for DSA. */
static
gpg_error_t
ssh_signature_encoder_dsa
(
ssh_key_type_spec_t
*
spec
,
estream_t
signature_blob
,
gcry_sexp_t
s_signature
)
{
gpg_error_t
err
=
0
;
gcry_sexp_t
valuelist
=
NULL
;
gcry_sexp_t
sublist
=
NULL
;
gcry_mpi_t
sig_value
=
NULL
;
gcry_mpi_t
*
mpis
=
NULL
;
const
char
*
elems
;
size_t
elems_n
;
int
i
;
unsigned
char
buffer
[
SSH_DSA_SIGNATURE_PADDING
*
SSH_DSA_SIGNATURE_ELEMS
];
unsigned
char
*
data
=
NULL
;
size_t
data_n
;
valuelist
=
gcry_sexp_nth
(
s_signature
,
1
);
if
(
!
valuelist
)
{
err
=
gpg_error
(
GPG_ERR_INV_SEXP
);
goto
out
;
}
elems
=
spec
->
elems_signature
;
elems_n
=
strlen
(
elems
);
mpis
=
xtrycalloc
(
elems_n
+
1
,
sizeof
*
mpis
);
if
(
!
mpis
)
{
err
=
gpg_error_from_syserror
();
goto
out
;
}
for
(
i
=
0
;
i
<
elems_n
;
i
++
)
{
sublist
=
gcry_sexp_find_token
(
valuelist
,
spec
->
elems_signature
+
i
,
1
);
if
(
!
sublist
)
{
err
=
gpg_error
(
GPG_ERR_INV_SEXP
);
break
;
}
sig_value
=
gcry_sexp_nth_mpi
(
sublist
,
1
,
GCRYMPI_FMT_USG
);
if
(
!
sig_value
)
{
err
=
gpg_error
(
GPG_ERR_INTERNAL
);
/* FIXME? */
break
;
}
gcry_sexp_release
(
sublist
);
sublist
=
NULL
;
mpis
[
i
]
=
sig_value
;
}
if
(
err
)
goto
out
;
/* DSA specific code. */
/* FIXME: Why this complicated code? Why collecting boths mpis in a
buffer instead of writing them out one after the other? */
for
(
i
=
0
;
i
<
2
;
i
++
)
{
err
=
gcry_mpi_aprint
(
GCRYMPI_FMT_USG
,
&
data
,
&
data_n
,
mpis
[
i
]);
if
(
err
)
break
;
if
(
data_n
>
SSH_DSA_SIGNATURE_PADDING
)
{
err
=
gpg_error
(
GPG_ERR_INTERNAL
);
/* FIXME? */
break
;
}
memset
(
buffer
+
(
i
*
SSH_DSA_SIGNATURE_PADDING
),
0
,
SSH_DSA_SIGNATURE_PADDING
-
data_n
);
memcpy
(
buffer
+
(
i
*
SSH_DSA_SIGNATURE_PADDING
)
+
(
SSH_DSA_SIGNATURE_PADDING
-
data_n
),
data
,
data_n
);
xfree
(
data
);
data
=
NULL
;
}
if
(
err
)
goto
out
;
err
=
stream_write_string
(
signature_blob
,
buffer
,
sizeof
(
buffer
));
out
:
xfree
(
data
);
gcry_sexp_release
(
valuelist
);
gcry_sexp_release
(
sublist
);
mpint_list_free
(
mpis
);
return
err
;
}
/* Signature encoder function for ECDSA. */
static
gpg_error_t
ssh_signature_encoder_ecdsa
(
ssh_key_type_spec_t
*
spec
,
estream_t
stream
,
gcry_sexp_t
s_signature
)
{
gpg_error_t
err
=
0
;
gcry_sexp_t
valuelist
=
NULL
;
gcry_sexp_t
sublist
=
NULL
;
gcry_mpi_t
sig_value
=
NULL
;
gcry_mpi_t
*
mpis
=
NULL
;
const
char
*
elems
;
size_t
elems_n
;
int
i
;
unsigned
char
*
data
[
2
]
=
{
NULL
,
NULL
};
size_t
data_n
[
2
];
size_t
innerlen
;
valuelist
=
gcry_sexp_nth
(
s_signature
,
1
);
if
(
!
valuelist
)
{
err
=
gpg_error
(
GPG_ERR_INV_SEXP
);
goto
out
;
}
elems
=
spec
->
elems_signature
;
elems_n
=
strlen
(
elems
);
mpis
=
xtrycalloc
(
elems_n
+
1
,
sizeof
*
mpis
);
if
(
!
mpis
)
{
err
=
gpg_error_from_syserror
();
goto
out
;
}
for
(
i
=
0
;
i
<
elems_n
;
i
++
)
{
sublist
=
gcry_sexp_find_token
(
valuelist
,
spec
->
elems_signature
+
i
,
1
);
if
(
!
sublist
)
{
err
=
gpg_error
(
GPG_ERR_INV_SEXP
);
break
;
}
sig_value
=
gcry_sexp_nth_mpi
(
sublist
,
1
,
GCRYMPI_FMT_USG
);
if
(
!
sig_value
)
{
err
=
gpg_error
(
GPG_ERR_INTERNAL
);
/* FIXME? */
break
;
}
gcry_sexp_release
(
sublist
);
sublist
=
NULL
;
mpis
[
i
]
=
sig_value
;
}
if
(
err
)
goto
out
;
/* ECDSA specific */
innerlen
=
0
;
for
(
i
=
0
;
i
<
DIM
(
data
);
i
++
)
{
err
=
gcry_mpi_aprint
(
GCRYMPI_FMT_STD
,
&
data
[
i
],
&
data_n
[
i
],
mpis
[
i
]);
if
(
err
)
goto
out
;
innerlen
+=
4
+
data_n
[
i
];
}
err
=
stream_write_uint32
(
stream
,
innerlen
);
if
(
err
)
goto
out
;
for
(
i
=
0
;
i
<
DIM
(
data
);
i
++
)
{
err
=
stream_write_string
(
stream
,
data
[
i
],
data_n
[
i
]);
if
(
err
)
goto
out
;
}
out
:
for
(
i
=
0
;
i
<
DIM
(
data
);
i
++
)
xfree
(
data
[
i
]);
gcry_sexp_release
(
valuelist
);
gcry_sexp_release
(
sublist
);
mpint_list_free
(
mpis
);
return
err
;
}
/* Signature encoder function for EdDSA. */
static
gpg_error_t
ssh_signature_encoder_eddsa
(
ssh_key_type_spec_t
*
spec
,
estream_t
stream
,
gcry_sexp_t
s_signature
)
{
gpg_error_t
err
=
0
;
gcry_sexp_t
valuelist
=
NULL
;
gcry_sexp_t
sublist
=
NULL
;
const
char
*
elems
;
size_t
elems_n
;
int
i
;
unsigned
char
*
data
[
2
]
=
{
NULL
,
NULL
};
size_t
data_n
[
2
];
size_t
totallen
=
0
;
valuelist
=
gcry_sexp_nth
(
s_signature
,
1
);
if
(
!
valuelist
)
{
err
=
gpg_error
(
GPG_ERR_INV_SEXP
);
goto
out
;
}
elems
=
spec
->
elems_signature
;
elems_n
=
strlen
(
elems
);
if
(
elems_n
!=
DIM
(
data
))
{
err
=
gpg_error
(
GPG_ERR_INV_SEXP
);
goto
out
;
}
for
(
i
=
0
;
i
<
DIM
(
data
);
i
++
)
{
sublist
=
gcry_sexp_find_token
(
valuelist
,
spec
->
elems_signature
+
i
,
1
);
if
(
!
sublist
)
{
err
=
gpg_error
(
GPG_ERR_INV_SEXP
);
break
;
}
data
[
i
]
=
gcry_sexp_nth_buffer
(
sublist
,
1
,
&
data_n
[
i
]);
if
(
!
data
[
i
])
{
err
=
gpg_error
(
GPG_ERR_INTERNAL
);
/* FIXME? */
break
;
}
totallen
+=
data_n
[
i
];
gcry_sexp_release
(
sublist
);
sublist
=
NULL
;
}
if
(
err
)
goto
out
;
err
=
stream_write_uint32
(
stream
,
totallen
);
if
(
err
)
goto
out
;
for
(
i
=
0
;
i
<
DIM
(
data
);
i
++
)
{
err
=
stream_write_data
(
stream
,
data
[
i
],
data_n
[
i
]);
if
(
err
)
goto
out
;
}
out
:
for
(
i
=
0
;
i
<
DIM
(
data
);
i
++
)
xfree
(
data
[
i
]);
gcry_sexp_release
(
valuelist
);
gcry_sexp_release
(
sublist
);
return
err
;
}
/*
S-Expressions.
*/
/* This function constructs a new S-Expression for the key identified
by the KEY_SPEC, SECRET, CURVE_NAME, MPIS, and COMMENT, which is to
be stored at R_SEXP. Returns an error code. */
static
gpg_error_t
sexp_key_construct
(
gcry_sexp_t
*
r_sexp
,
ssh_key_type_spec_t
key_spec
,
int
secret
,
const
char
*
curve_name
,
gcry_mpi_t
*
mpis
,
const
char
*
comment
)
{
gpg_error_t
err
;
gcry_sexp_t
sexp_new
=
NULL
;
void
*
formatbuf
=
NULL
;
void
**
arg_list
=
NULL
;
estream_t
format
=
NULL
;
char
*
algo_name
=
NULL
;
if
((
key_spec
.
flags
&
SPEC_FLAG_IS_EdDSA
))
{
/* It is much easier and more readable to use a separate code
path for EdDSA. */
if
(
!
curve_name
)
err
=
gpg_error
(
GPG_ERR_INV_CURVE
);
else
if
(
!
mpis
[
0
]
||
!
gcry_mpi_get_flag
(
mpis
[
0
],
GCRYMPI_FLAG_OPAQUE
))
err
=
gpg_error
(
GPG_ERR_BAD_PUBKEY
);
else
if
(
secret
&&
(
!
mpis
[
1
]
||
!
gcry_mpi_get_flag
(
mpis
[
1
],
GCRYMPI_FLAG_OPAQUE
)))
err
=
gpg_error
(
GPG_ERR_BAD_SECKEY
);
else
if
(
secret
)
err
=
gcry_sexp_build
(
&
sexp_new
,
NULL
,
"(private-key(ecc(curve %s)"
"(flags eddsa)(q %m)(d %m))"
"(comment%s))"
,
curve_name
,
mpis
[
0
],
mpis
[
1
],
comment
?
comment
:
""
);
else
err
=
gcry_sexp_build
(
&
sexp_new
,
NULL
,
"(public-key(ecc(curve %s)"
"(flags eddsa)(q %m))"
"(comment%s))"
,
curve_name
,
mpis
[
0
],
comment
?
comment
:
""
);
}
else
{
const
char
*
key_identifier
[]
=
{
"public-key"
,
"private-key"
};
int
arg_idx
;
const
char
*
elems
;
size_t
elems_n
;
unsigned
int
i
,
j
;
if
(
secret
)
elems
=
key_spec
.
elems_sexp_order
;
else
elems
=
key_spec
.
elems_key_public
;
elems_n
=
strlen
(
elems
);
format
=
es_fopenmem
(
0
,
"a+b"
);
if
(
!
format
)
{
err
=
gpg_error_from_syserror
();
goto
out
;
}
/* Key identifier, algorithm identifier, mpis, comment, and a NULL
as a safeguard. */
arg_list
=
xtrymalloc
(
sizeof
(
*
arg_list
)
*
(
2
+
1
+
elems_n
+
1
+
1
));
if
(
!
arg_list
)
{
err
=
gpg_error_from_syserror
();
goto
out
;
}
arg_idx
=
0
;
es_fputs
(
"(%s(%s"
,
format
);
arg_list
[
arg_idx
++
]
=
&
key_identifier
[
secret
];
algo_name
=
xtrystrdup
(
gcry_pk_algo_name
(
key_spec
.
algo
));
if
(
!
algo_name
)
{
err
=
gpg_error_from_syserror
();
goto
out
;
}
strlwr
(
algo_name
);
arg_list
[
arg_idx
++
]
=
&
algo_name
;
if
(
curve_name
)
{
es_fputs
(
"(curve%s)"
,
format
);
arg_list
[
arg_idx
++
]
=
&
curve_name
;
}
for
(
i
=
0
;
i
<
elems_n
;
i
++
)
{
es_fprintf
(
format
,
"(%c%%m)"
,
elems
[
i
]);
if
(
secret
)
{
for
(
j
=
0
;
j
<
elems_n
;
j
++
)
if
(
key_spec
.
elems_key_secret
[
j
]
==
elems
[
i
])
break
;
}
else
j
=
i
;
arg_list
[
arg_idx
++
]
=
&
mpis
[
j
];
}
es_fputs
(
")(comment%s))"
,
format
);
arg_list
[
arg_idx
++
]
=
&
comment
;
arg_list
[
arg_idx
]
=
NULL
;
es_putc
(
0
,
format
);
if
(
es_ferror
(
format
))
{
err
=
gpg_error_from_syserror
();
goto
out
;
}
if
(
es_fclose_snatch
(
format
,
&
formatbuf
,
NULL
))
{
err
=
gpg_error_from_syserror
();
goto
out
;
}
format
=
NULL
;
err
=
gcry_sexp_build_array
(
&
sexp_new
,
NULL
,
formatbuf
,
arg_list
);
}
if
(
!
err
)
*
r_sexp
=
sexp_new
;
out
:
es_fclose
(
format
);
xfree
(
arg_list
);
xfree
(
formatbuf
);
xfree
(
algo_name
);
return
err
;
}
/* This function extracts the key from the s-expression SEXP according
to KEY_SPEC and stores it in ssh format at (R_BLOB, R_BLOBLEN). If
WITH_SECRET is true, the secret key parts are also extracted if
possible. Returns 0 on success or an error code. Note that data
stored at R_BLOB must be freed using es_free! */
static
gpg_error_t
ssh_key_to_blob
(
gcry_sexp_t
sexp
,
int
with_secret
,
ssh_key_type_spec_t
key_spec
,
void
**
r_blob
,
size_t
*
r_blob_size
)
{
gpg_error_t
err
=
0
;
gcry_sexp_t
value_list
=
NULL
;
gcry_sexp_t
value_pair
=
NULL
;
char
*
curve_name
=
NULL
;
estream_t
stream
=
NULL
;
void
*
blob
=
NULL
;
size_t
blob_size
;
const
char
*
elems
,
*
p_elems
;
const
char
*
data
;
size_t
datalen
;
*
r_blob
=
NULL
;
*
r_blob_size
=
0
;
stream
=
es_fopenmem
(
0
,
"r+b"
);
if
(
!
stream
)
{
err
=
gpg_error_from_syserror
();
goto
out
;
}
/* Get the type of the key extpression. */
data
=
gcry_sexp_nth_data
(
sexp
,
0
,
&
datalen
);
if
(
!
data
)
{
err
=
gpg_error
(
GPG_ERR_INV_SEXP
);
goto
out
;
}
if
((
datalen
==
10
&&
!
strncmp
(
data
,
"public-key"
,
10
))
||
(
datalen
==
21
&&
!
strncmp
(
data
,
"protected-private-key"
,
21
))
||
(
datalen
==
20
&&
!
strncmp
(
data
,
"shadowed-private-key"
,
20
)))
elems
=
key_spec
.
elems_key_public
;
else
if
(
datalen
==
11
&&
!
strncmp
(
data
,
"private-key"
,
11
))
elems
=
with_secret
?
key_spec
.
elems_key_secret
:
key_spec
.
elems_key_public
;
else
{
err
=
gpg_error
(
GPG_ERR_INV_SEXP
);
goto
out
;
}
/* Get key value list. */
value_list
=
gcry_sexp_cadr
(
sexp
);
if
(
!
value_list
)
{
err
=
gpg_error
(
GPG_ERR_INV_SEXP
);
goto
out
;
}
/* Write the ssh algorithm identifier. */
if
((
key_spec
.
flags
&
SPEC_FLAG_IS_ECDSA
))
{
/* Parse the "curve" parameter. We currently expect the curve
name for ECC and not the parameters of the curve. This can
easily be changed but then we need to find the curve name
from the parameters using gcry_pk_get_curve. */
const
char
*
mapped
;
const
char
*
sshname
;
gcry_sexp_release
(
value_pair
);
value_pair
=
gcry_sexp_find_token
(
value_list
,
"curve"
,
5
);
if
(
!
value_pair
)
{
err
=
gpg_error
(
GPG_ERR_INV_CURVE
);
goto
out
;
}
curve_name
=
gcry_sexp_nth_string
(
value_pair
,
1
);
if
(
!
curve_name
)
{
err
=
gpg_error
(
GPG_ERR_INV_CURVE
);
/* (Or out of core.) */
goto
out
;
}
/* Fixme: The mapping should be done by using gcry_pk_get_curve
et al to iterate over all name aliases. */
if
(
!
strcmp
(
curve_name
,
"NIST P-256"
))
mapped
=
"nistp256"
;
else
if
(
!
strcmp
(
curve_name
,
"NIST P-384"
))
mapped
=
"nistp384"
;
else
if
(
!
strcmp
(
curve_name
,
"NIST P-521"
))
mapped
=
"nistp521"
;
else
mapped
=
NULL
;
if
(
mapped
)
{
xfree
(
curve_name
);
curve_name
=
xtrystrdup
(
mapped
);
if
(
!
curve_name
)
{
err
=
gpg_error_from_syserror
();
goto
out
;
}
}
sshname
=
ssh_identifier_from_curve_name
(
curve_name
);
if
(
!
sshname
)
{
err
=
gpg_error
(
GPG_ERR_UNKNOWN_CURVE
);
goto
out
;
}
err
=
stream_write_cstring
(
stream
,
sshname
);
if
(
err
)
goto
out
;
err
=
stream_write_cstring
(
stream
,
curve_name
);
if
(
err
)
goto
out
;
}
else
{
/* Note: This is also used for EdDSA. */
err
=
stream_write_cstring
(
stream
,
key_spec
.
ssh_identifier
);
if
(
err
)
goto
out
;
}
/* Write the parameters. */
for
(
p_elems
=
elems
;
*
p_elems
;
p_elems
++
)
{
gcry_sexp_release
(
value_pair
);
value_pair
=
gcry_sexp_find_token
(
value_list
,
p_elems
,
1
);
if
(
!
value_pair
)
{
err
=
gpg_error
(
GPG_ERR_INV_SEXP
);
goto
out
;
}
if
((
key_spec
.
flags
&
SPEC_FLAG_IS_EdDSA
))
{
data
=
gcry_sexp_nth_data
(
value_pair
,
1
,
&
datalen
);
if
(
!
data
)
{
err
=
gpg_error
(
GPG_ERR_INV_SEXP
);
goto
out
;
}
if
(
*
p_elems
==
'q'
&&
datalen
)
{
/* Remove the prefix 0x40. */
data
++
;
datalen
--
;
}
err
=
stream_write_string
(
stream
,
data
,
datalen
);
if
(
err
)
goto
out
;
}
else
{
gcry_mpi_t
mpi
;
/* Note that we need to use STD format; i.e. prepend a 0x00
to indicate a positive number if the high bit is set. */
mpi
=
gcry_sexp_nth_mpi
(
value_pair
,
1
,
GCRYMPI_FMT_STD
);
if
(
!
mpi
)
{
err
=
gpg_error
(
GPG_ERR_INV_SEXP
);
goto
out
;
}
err
=
stream_write_mpi
(
stream
,
mpi
);
gcry_mpi_release
(
mpi
);
if
(
err
)
goto
out
;
}
}
if
(
es_fclose_snatch
(
stream
,
&
blob
,
&
blob_size
))
{
err
=
gpg_error_from_syserror
();
goto
out
;
}
stream
=
NULL
;
*
r_blob
=
blob
;
blob
=
NULL
;
*
r_blob_size
=
blob_size
;
out
:
gcry_sexp_release
(
value_list
);
gcry_sexp_release
(
value_pair
);
xfree
(
curve_name
);
es_fclose
(
stream
);
es_free
(
blob
);
return
err
;
}
/*
Key I/O.
*/
/* Search for a key specification entry. If SSH_NAME is not NULL,
search for an entry whose "ssh_name" is equal to SSH_NAME;
otherwise, search for an entry whose algorithm is equal to ALGO.
Store found entry in SPEC on success, return error otherwise. */
static
gpg_error_t
ssh_key_type_lookup
(
const
char
*
ssh_name
,
int
algo
,
ssh_key_type_spec_t
*
spec
)
{
gpg_error_t
err
;
unsigned
int
i
;
for
(
i
=
0
;
i
<
DIM
(
ssh_key_types
);
i
++
)
if
((
ssh_name
&&
(
!
strcmp
(
ssh_name
,
ssh_key_types
[
i
].
ssh_identifier
)))
||
algo
==
ssh_key_types
[
i
].
algo
)
break
;
if
(
i
==
DIM
(
ssh_key_types
))
err
=
gpg_error
(
GPG_ERR_NOT_FOUND
);
else
{
*
spec
=
ssh_key_types
[
i
];
err
=
0
;
}
return
err
;
}
/* Receive a key from STREAM, according to the key specification given
as KEY_SPEC. Depending on SECRET, receive a secret or a public
key. If READ_COMMENT is true, receive a comment string as well.
Constructs a new S-Expression from received data and stores it in
KEY_NEW. Returns zero on success or an error code. */
static
gpg_error_t
ssh_receive_key
(
estream_t
stream
,
gcry_sexp_t
*
key_new
,
int
secret
,
int
read_comment
,
ssh_key_type_spec_t
*
key_spec
)
{
gpg_error_t
err
;
char
*
key_type
=
NULL
;
char
*
comment
=
NULL
;
estream_t
cert
=
NULL
;
gcry_sexp_t
key
=
NULL
;
ssh_key_type_spec_t
spec
;
gcry_mpi_t
*
mpi_list
=
NULL
;
const
char
*
elems
;
char
*
curve_name
=
NULL
;
err
=
stream_read_cstring
(
stream
,
&
key_type
);
if
(
err
)
goto
out
;
err
=
ssh_key_type_lookup
(
key_type
,
0
,
&
spec
);
if
(
err
)
goto
out
;
if
((
spec
.
flags
&
SPEC_FLAG_WITH_CERT
))
{
/* This is an OpenSSH certificate+private key. The certificate
is an SSH string and which we store in an estream object. */
unsigned
char
*
buffer
;
u32
buflen
;
char
*
cert_key_type
;
err
=
stream_read_string
(
stream
,
0
,
&
buffer
,
&
buflen
);
if
(
err
)
goto
out
;
cert
=
es_fopenmem_init
(
0
,
"rb"
,
buffer
,
buflen
);
xfree
(
buffer
);
if
(
!
cert
)
{
err
=
gpg_error_from_syserror
();
goto
out
;
}
/* Check that the key type matches. */
err
=
stream_read_cstring
(
cert
,
&
cert_key_type
);
if
(
err
)
goto
out
;
if
(
strcmp
(
cert_key_type
,
key_type
)
)
{
xfree
(
cert_key_type
);
log_error
(
"key types in received ssh certificate do not match
\n
"
);
err
=
gpg_error
(
GPG_ERR_INV_CERT_OBJ
);
goto
out
;
}
xfree
(
cert_key_type
);
/* Skip the nonce. */
err
=
stream_read_string
(
cert
,
0
,
NULL
,
NULL
);
if
(
err
)
goto
out
;
}
if
((
spec
.
flags
&
SPEC_FLAG_IS_EdDSA
))
{
/* The format of an EdDSA key is:
* string key_type ("ssh-ed25519")
* string public_key
* string private_key
*
* Note that the private key is the concatenation of the private
* key with the public key. Thus theres are 64 bytes; however
* we only want the real 32 byte private key - Libgcrypt expects
* this.
*/
mpi_list
=
xtrycalloc
(
3
,
sizeof
*
mpi_list
);
if
(
!
mpi_list
)
{
err
=
gpg_error_from_syserror
();
goto
out
;
}
err
=
stream_read_blob
(
cert
?
cert
:
stream
,
0
,
&
mpi_list
[
0
]);
if
(
err
)
goto
out
;
if
(
secret
)
{
u32
len
=
0
;
unsigned
char
*
buffer
;
/* Read string length. */
err
=
stream_read_uint32
(
stream
,
&
len
);
if
(
err
)
goto
out
;
if
(
len
!=
32
&&
len
!=
64
)
{
err
=
gpg_error
(
GPG_ERR_BAD_SECKEY
);
goto
out
;
}
buffer
=
xtrymalloc_secure
(
32
);
if
(
!
buffer
)
{
err
=
gpg_error_from_syserror
();
goto
out
;
}
err
=
stream_read_data
(
stream
,
buffer
,
32
);
if
(
err
)
{
xfree
(
buffer
);
goto
out
;
}
mpi_list
[
1
]
=
gcry_mpi_set_opaque
(
NULL
,
buffer
,
8
*
32
);
buffer
=
NULL
;
if
(
len
==
64
)
{
err
=
stream_read_skip
(
stream
,
32
);
if
(
err
)
goto
out
;
}
}
}
else
if
((
spec
.
flags
&
SPEC_FLAG_IS_ECDSA
))
{
/* The format of an ECDSA key is:
* string key_type ("ecdsa-sha2-nistp256" |
* "ecdsa-sha2-nistp384" |
* "ecdsa-sha2-nistp521" )
* string ecdsa_curve_name
* string ecdsa_public_key
* mpint ecdsa_private
*
* Note that we use the mpint reader instead of the string
* reader for ecsa_public_key. For the certificate variante
* ecdsa_curve_name+ecdsa_public_key are replaced by the
* certificate.
*/
unsigned
char
*
buffer
;
const
char
*
mapped
;
err
=
stream_read_string
(
cert
?
cert
:
stream
,
0
,
&
buffer
,
NULL
);
if
(
err
)
goto
out
;
curve_name
=
buffer
;
/* Fixme: Check that curve_name matches the keytype. */
/* Because Libgcrypt < 1.6 has no support for the "nistpNNN"
curve names, we need to translate them here to Libgcrypt's
native names. */
if
(
!
strcmp
(
curve_name
,
"nistp256"
))
mapped
=
"NIST P-256"
;
else
if
(
!
strcmp
(
curve_name
,
"nistp384"
))
mapped
=
"NIST P-384"
;
else
if
(
!
strcmp
(
curve_name
,
"nistp521"
))
mapped
=
"NIST P-521"
;
else
mapped
=
NULL
;
if
(
mapped
)
{
xfree
(
curve_name
);
curve_name
=
xtrystrdup
(
mapped
);
if
(
!
curve_name
)
{
err
=
gpg_error_from_syserror
();
goto
out
;
}
}
err
=
ssh_receive_mpint_list
(
stream
,
secret
,
&
spec
,
cert
,
&
mpi_list
);
if
(
err
)
goto
out
;
}
else
{
err
=
ssh_receive_mpint_list
(
stream
,
secret
,
&
spec
,
cert
,
&
mpi_list
);
if
(
err
)
goto
out
;
}
if
(
read_comment
)
{
err
=
stream_read_cstring
(
stream
,
&
comment
);
if
(
err
)
goto
out
;
}
if
(
secret
)
elems
=
spec
.
elems_key_secret
;
else
elems
=
spec
.
elems_key_public
;
if
(
spec
.
key_modifier
)
{
err
=
(
*
spec
.
key_modifier
)
(
elems
,
mpi_list
);
if
(
err
)
goto
out
;
}
if
((
spec
.
flags
&
SPEC_FLAG_IS_EdDSA
))
{
if
(
secret
)
{
err
=
gcry_sexp_build
(
&
key
,
NULL
,
"(private-key(ecc(curve
\"
Ed25519
\"
)"
"(flags eddsa)(q %m)(d %m))"
"(comment%s))"
,
mpi_list
[
0
],
mpi_list
[
1
],
comment
?
comment
:
""
);
}
else
{
err
=
gcry_sexp_build
(
&
key
,
NULL
,
"(public-key(ecc(curve
\"
Ed25519
\"
)"
"(flags eddsa)(q %m))"
"(comment%s))"
,
mpi_list
[
0
],
comment
?
comment
:
""
);
}
}
else
{
err
=
sexp_key_construct
(
&
key
,
spec
,
secret
,
curve_name
,
mpi_list
,
comment
?
comment
:
""
);
if
(
err
)
goto
out
;
}
if
(
key_spec
)
*
key_spec
=
spec
;
*
key_new
=
key
;
out
:
es_fclose
(
cert
);
mpint_list_free
(
mpi_list
);
xfree
(
curve_name
);
xfree
(
key_type
);
xfree
(
comment
);
return
err
;
}
/* Write the public key from KEY to STREAM in SSH key format. If
OVERRIDE_COMMENT is not NULL, it will be used instead of the
comment stored in the key. */
static
gpg_error_t
ssh_send_key_public
(
estream_t
stream
,
gcry_sexp_t
key
,
const
char
*
override_comment
)
{
ssh_key_type_spec_t
spec
;
int
algo
;
char
*
comment
=
NULL
;
void
*
blob
=
NULL
;
size_t
bloblen
;
gpg_error_t
err
=
0
;
algo
=
get_pk_algo_from_key
(
key
);
if
(
algo
==
0
)
goto
out
;
err
=
ssh_key_type_lookup
(
NULL
,
algo
,
&
spec
);
if
(
err
)
goto
out
;
err
=
ssh_key_to_blob
(
key
,
0
,
spec
,
&
blob
,
&
bloblen
);
if
(
err
)
goto
out
;
err
=
stream_write_string
(
stream
,
blob
,
bloblen
);
if
(
err
)
goto
out
;
if
(
override_comment
)
err
=
stream_write_cstring
(
stream
,
override_comment
);
else
{
err
=
ssh_key_extract_comment
(
key
,
&
comment
);
if
(
err
)
err
=
stream_write_cstring
(
stream
,
"(none)"
);
else
err
=
stream_write_cstring
(
stream
,
comment
);
}
if
(
err
)
goto
out
;
out
:
xfree
(
comment
);
es_free
(
blob
);
return
err
;
}
/* Read a public key out of BLOB/BLOB_SIZE according to the key
specification given as KEY_SPEC, storing the new key in KEY_PUBLIC.
Returns zero on success or an error code. */
static
gpg_error_t
ssh_read_key_public_from_blob
(
unsigned
char
*
blob
,
size_t
blob_size
,
gcry_sexp_t
*
key_public
,
ssh_key_type_spec_t
*
key_spec
)
{
gpg_error_t
err
;
estream_t
blob_stream
;
blob_stream
=
es_fopenmem
(
0
,
"r+b"
);
if
(
!
blob_stream
)
{
err
=
gpg_error_from_syserror
();
goto
out
;
}
err
=
stream_write_data
(
blob_stream
,
blob
,
blob_size
);
if
(
err
)
goto
out
;
err
=
es_fseek
(
blob_stream
,
0
,
SEEK_SET
);
if
(
err
)
goto
out
;
err
=
ssh_receive_key
(
blob_stream
,
key_public
,
0
,
0
,
key_spec
);
out
:
es_fclose
(
blob_stream
);
return
err
;
}
/* This function calculates the key grip for the key contained in the
S-Expression KEY and writes it to BUFFER, which must be large
enough to hold it. Returns usual error code. */
static
gpg_error_t
ssh_key_grip
(
gcry_sexp_t
key
,
unsigned
char
*
buffer
)
{
if
(
!
gcry_pk_get_keygrip
(
key
,
buffer
))
{
gpg_error_t
err
=
gcry_pk_testkey
(
key
);
return
err
?
err
:
gpg_error
(
GPG_ERR_INTERNAL
);
}
return
0
;
}
static
gpg_error_t
card_key_list
(
ctrl_t
ctrl
,
char
**
r_serialno
,
strlist_t
*
result
)
{
gpg_error_t
err
;
*
r_serialno
=
NULL
;
*
result
=
NULL
;
err
=
agent_card_serialno
(
ctrl
,
r_serialno
,
NULL
);
if
(
err
)
{
if
(
gpg_err_code
(
err
)
!=
GPG_ERR_ENODEV
&&
opt
.
verbose
)
log_info
(
_
(
"error getting serial number of card: %s
\n
"
),
gpg_strerror
(
err
));
/* Nothing available. */
return
0
;
}
err
=
agent_card_cardlist
(
ctrl
,
result
);
if
(
err
)
{
xfree
(
*
r_serialno
);
*
r_serialno
=
NULL
;
}
return
err
;
}
/* Check whether a smartcard is available and whether it has a usable
key. Store a copy of that key at R_PK and return 0. If no key is
available store NULL at R_PK and return an error code. If CARDSN
is not NULL, a string with the serial number of the card will be
a malloced and stored there. */
static
gpg_error_t
card_key_available
(
ctrl_t
ctrl
,
gcry_sexp_t
*
r_pk
,
char
**
cardsn
)
{
gpg_error_t
err
;
char
*
authkeyid
;
char
*
serialno
=
NULL
;
unsigned
char
*
pkbuf
;
size_t
pkbuflen
;
gcry_sexp_t
s_pk
;
unsigned
char
grip
[
20
];
*
r_pk
=
NULL
;
if
(
cardsn
)
*
cardsn
=
NULL
;
/* First see whether a card is available and whether the application
is supported. */
err
=
agent_card_getattr
(
ctrl
,
"$AUTHKEYID"
,
&
authkeyid
);
if
(
gpg_err_code
(
err
)
==
GPG_ERR_CARD_REMOVED
)
{
/* Ask for the serial number to reset the card. */
err
=
agent_card_serialno
(
ctrl
,
&
serialno
,
NULL
);
if
(
err
)
{
if
(
opt
.
verbose
)
log_info
(
_
(
"error getting serial number of card: %s
\n
"
),
gpg_strerror
(
err
));
return
err
;
}
log_info
(
_
(
"detected card with S/N: %s
\n
"
),
serialno
);
err
=
agent_card_getattr
(
ctrl
,
"$AUTHKEYID"
,
&
authkeyid
);
}
if
(
err
)
{
log_error
(
_
(
"no authentication key for ssh on card: %s
\n
"
),
gpg_strerror
(
err
));
xfree
(
serialno
);
return
err
;
}
/* Get the S/N if we don't have it yet. Use the fast getattr method. */
if
(
!
serialno
&&
(
err
=
agent_card_getattr
(
ctrl
,
"SERIALNO"
,
&
serialno
))
)
{
log_error
(
_
(
"error getting serial number of card: %s
\n
"
),
gpg_strerror
(
err
));
xfree
(
authkeyid
);
return
err
;
}
/* Read the public key. */
err
=
agent_card_readkey
(
ctrl
,
authkeyid
,
&
pkbuf
);
if
(
err
)
{
if
(
opt
.
verbose
)
log_info
(
_
(
"no suitable card key found: %s
\n
"
),
gpg_strerror
(
err
));
xfree
(
serialno
);
xfree
(
authkeyid
);
return
err
;
}
pkbuflen
=
gcry_sexp_canon_len
(
pkbuf
,
0
,
NULL
,
NULL
);
err
=
gcry_sexp_sscan
(
&
s_pk
,
NULL
,
(
char
*
)
pkbuf
,
pkbuflen
);
if
(
err
)
{
log_error
(
"failed to build S-Exp from received card key: %s
\n
"
,
gpg_strerror
(
err
));
xfree
(
pkbuf
);
xfree
(
serialno
);
xfree
(
authkeyid
);
return
err
;
}
err
=
ssh_key_grip
(
s_pk
,
grip
);
if
(
err
)
{
log_debug
(
"error computing keygrip from received card key: %s
\n
"
,
gcry_strerror
(
err
));
xfree
(
pkbuf
);
gcry_sexp_release
(
s_pk
);
xfree
(
serialno
);
xfree
(
authkeyid
);
return
err
;
}
if
(
agent_key_available
(
grip
)
)
{
/* (Shadow)-key is not available in our key storage. */
err
=
agent_write_shadow_key
(
grip
,
serialno
,
authkeyid
,
pkbuf
,
0
);
if
(
err
)
{
xfree
(
pkbuf
);
gcry_sexp_release
(
s_pk
);
xfree
(
serialno
);
xfree
(
authkeyid
);
return
err
;
}
}
if
(
cardsn
)
{
char
*
dispsn
;
/* If the card handler is able to return a short serialnumber,
use that one, else use the complete serialno. */
if
(
!
agent_card_getattr
(
ctrl
,
"$DISPSERIALNO"
,
&
dispsn
))
{
*
cardsn
=
xtryasprintf
(
"cardno:%s"
,
dispsn
);
xfree
(
dispsn
);
}
else
*
cardsn
=
xtryasprintf
(
"cardno:%s"
,
serialno
);
if
(
!*
cardsn
)
{
err
=
gpg_error_from_syserror
();
xfree
(
pkbuf
);
gcry_sexp_release
(
s_pk
);
xfree
(
serialno
);
xfree
(
authkeyid
);
return
err
;
}
}
xfree
(
pkbuf
);
xfree
(
serialno
);
xfree
(
authkeyid
);
*
r_pk
=
s_pk
;
return
0
;
}
/*
Request handler. Each handler is provided with a CTRL context, a
REQUEST object and a RESPONSE object. The actual request is to be
read from REQUEST, the response needs to be written to RESPONSE.
*/
/* Handler for the "request_identities" command. */
static
gpg_error_t
ssh_handler_request_identities
(
ctrl_t
ctrl
,
estream_t
request
,
estream_t
response
)
{
u32
key_counter
;
estream_t
key_blobs
;
gcry_sexp_t
key_public
;
gpg_error_t
err
;
int
ret
;
ssh_control_file_t
cf
=
NULL
;
gpg_error_t
ret_err
;
(
void
)
request
;
/* Prepare buffer stream. */
key_public
=
NULL
;
key_counter
=
0
;
err
=
0
;
key_blobs
=
es_fopenmem
(
0
,
"r+b"
);
if
(
!
key_blobs
)
{
err
=
gpg_error_from_syserror
();
goto
out
;
}
/* First check whether a key is currently available in the card
reader - this should be allowed even without being listed in
sshcontrol. */
if
(
!
opt
.
disable_scdaemon
)
{
char
*
serialno
;
strlist_t
card_list
,
sl
;
err
=
card_key_list
(
ctrl
,
&
serialno
,
&
card_list
);
if
(
err
)
{
if
(
opt
.
verbose
)
log_info
(
_
(
"error getting list of cards: %s
\n
"
),
gpg_strerror
(
err
));
goto
scd_out
;
}
for
(
sl
=
card_list
;
sl
;
sl
=
sl
->
next
)
{
char
*
serialno0
;
char
*
cardsn
;
err
=
agent_card_serialno
(
ctrl
,
&
serialno0
,
sl
->
d
);
if
(
err
)
{
if
(
opt
.
verbose
)
log_info
(
_
(
"error getting serial number of card: %s
\n
"
),
gpg_strerror
(
err
));
continue
;
}
xfree
(
serialno0
);
if
(
card_key_available
(
ctrl
,
&
key_public
,
&
cardsn
))
continue
;
err
=
ssh_send_key_public
(
key_blobs
,
key_public
,
cardsn
);
gcry_sexp_release
(
key_public
);
key_public
=
NULL
;
xfree
(
cardsn
);
if
(
err
)
{
xfree
(
serialno
);
free_strlist
(
card_list
);
goto
out
;
}
key_counter
++
;
}
xfree
(
serialno
);
free_strlist
(
card_list
);
}
scd_out
:
/* Then look at all the registered and non-disabled keys. */
err
=
open_control_file
(
&
cf
,
0
);
if
(
err
)
goto
out
;
while
(
!
read_control_file_item
(
cf
))
{
unsigned
char
grip
[
20
];
if
(
!
cf
->
item
.
valid
)
continue
;
/* Should not happen. */
if
(
cf
->
item
.
disabled
)
continue
;
assert
(
strlen
(
cf
->
item
.
hexgrip
)
==
40
);
hex2bin
(
cf
->
item
.
hexgrip
,
grip
,
sizeof
(
grip
));
err
=
agent_public_key_from_file
(
ctrl
,
grip
,
&
key_public
);
if
(
err
)
{
log_error
(
"%s:%d: key '%s' skipped: %s
\n
"
,
cf
->
fname
,
cf
->
lnr
,
cf
->
item
.
hexgrip
,
gpg_strerror
(
err
));
continue
;
}
err
=
ssh_send_key_public
(
key_blobs
,
key_public
,
NULL
);
if
(
err
)
goto
out
;
gcry_sexp_release
(
key_public
);
key_public
=
NULL
;
key_counter
++
;
}
err
=
0
;
ret
=
es_fseek
(
key_blobs
,
0
,
SEEK_SET
);
if
(
ret
)
{
err
=
gpg_error_from_syserror
();
goto
out
;
}
out
:
/* Send response. */
gcry_sexp_release
(
key_public
);
if
(
!
err
)
{
ret_err
=
stream_write_byte
(
response
,
SSH_RESPONSE_IDENTITIES_ANSWER
);
if
(
!
ret_err
)
ret_err
=
stream_write_uint32
(
response
,
key_counter
);
if
(
!
ret_err
)
ret_err
=
stream_copy
(
response
,
key_blobs
);
}
else
{
ret_err
=
stream_write_byte
(
response
,
SSH_RESPONSE_FAILURE
);
}
es_fclose
(
key_blobs
);
close_control_file
(
cf
);
return
ret_err
;
}
/* This function hashes the data contained in DATA of size DATA_N
according to the message digest algorithm specified by MD_ALGORITHM
and writes the message digest to HASH, which needs to large enough
for the digest. */
static
gpg_error_t
data_hash
(
unsigned
char
*
data
,
size_t
data_n
,
int
md_algorithm
,
unsigned
char
*
hash
)
{
gcry_md_hash_buffer
(
md_algorithm
,
hash
,
data
,
data_n
);
return
0
;
}
/* This function signs the data described by CTRL. If HASH is not
NULL, (HASH,HASHLEN) overrides the hash stored in CTRL. This is to
allow the use of signature algorithms that implement the hashing
internally (e.g. Ed25519). On success the created signature is
stored in ssh format at R_SIG and it's size at R_SIGLEN; the caller
must use es_free to releaase this memory. */
static
gpg_error_t
data_sign
(
ctrl_t
ctrl
,
ssh_key_type_spec_t
*
spec
,
const
void
*
hash
,
size_t
hashlen
,
unsigned
char
**
r_sig
,
size_t
*
r_siglen
)
{
gpg_error_t
err
;
gcry_sexp_t
signature_sexp
=
NULL
;
estream_t
stream
=
NULL
;
void
*
blob
=
NULL
;
size_t
bloblen
;
char
hexgrip
[
40
+
1
];
*
r_sig
=
NULL
;
*
r_siglen
=
0
;
/* Quick check to see whether we have a valid keygrip and convert it
to hex. */
if
(
!
ctrl
->
have_keygrip
)
{
err
=
gpg_error
(
GPG_ERR_NO_SECKEY
);
goto
out
;
}
bin2hex
(
ctrl
->
keygrip
,
20
,
hexgrip
);
/* Ask for confirmation if needed. */
if
(
confirm_flag_from_sshcontrol
(
hexgrip
))
{
gcry_sexp_t
key
;
char
*
fpr
,
*
prompt
;
char
*
comment
=
NULL
;
err
=
agent_raw_key_from_file
(
ctrl
,
ctrl
->
keygrip
,
&
key
);
if
(
err
)
goto
out
;
err
=
ssh_get_fingerprint_string
(
key
,
&
fpr
);
if
(
!
err
)
{
gcry_sexp_t
tmpsxp
=
gcry_sexp_find_token
(
key
,
"comment"
,
0
);
if
(
tmpsxp
)
comment
=
gcry_sexp_nth_string
(
tmpsxp
,
1
);
gcry_sexp_release
(
tmpsxp
);
}
gcry_sexp_release
(
key
);
if
(
err
)
goto
out
;
prompt
=
xtryasprintf
(
L_
(
"An ssh process requested the use of key%%0A"
" %s%%0A"
" (%s)%%0A"
"Do you want to allow this?"
),
fpr
,
comment
?
comment
:
""
);
xfree
(
fpr
);
gcry_free
(
comment
);
err
=
agent_get_confirmation
(
ctrl
,
prompt
,
L_
(
"Allow"
),
L_
(
"Deny"
),
0
);
xfree
(
prompt
);
if
(
err
)
goto
out
;
}
/* Create signature. */
ctrl
->
use_auth_call
=
1
;
err
=
agent_pksign_do
(
ctrl
,
NULL
,
L_
(
"Please enter the passphrase "
"for the ssh key%%0A %F%%0A (%c)"
),
&
signature_sexp
,
CACHE_MODE_SSH
,
ttl_from_sshcontrol
,
hash
,
hashlen
);
ctrl
->
use_auth_call
=
0
;
if
(
err
)
goto
out
;
stream
=
es_fopenmem
(
0
,
"r+b"
);
if
(
!
stream
)
{
err
=
gpg_error_from_syserror
();
goto
out
;
}
err
=
stream_write_cstring
(
stream
,
spec
->
ssh_identifier
);
if
(
err
)
goto
out
;
err
=
spec
->
signature_encoder
(
spec
,
stream
,
signature_sexp
);
if
(
err
)
goto
out
;
err
=
es_fclose_snatch
(
stream
,
&
blob
,
&
bloblen
);
if
(
err
)
goto
out
;
stream
=
NULL
;
*
r_sig
=
blob
;
blob
=
NULL
;
*
r_siglen
=
bloblen
;
out
:
xfree
(
blob
);
es_fclose
(
stream
);
gcry_sexp_release
(
signature_sexp
);
return
err
;
}
/* Handler for the "sign_request" command. */
static
gpg_error_t
ssh_handler_sign_request
(
ctrl_t
ctrl
,
estream_t
request
,
estream_t
response
)
{
gcry_sexp_t
key
=
NULL
;
ssh_key_type_spec_t
spec
;
unsigned
char
hash
[
MAX_DIGEST_LEN
];
unsigned
int
hash_n
;
unsigned
char
key_grip
[
20
];
unsigned
char
*
key_blob
=
NULL
;
u32
key_blob_size
;
unsigned
char
*
data
=
NULL
;
unsigned
char
*
sig
=
NULL
;
size_t
sig_n
;
u32
data_size
;
u32
flags
;
gpg_error_t
err
;
gpg_error_t
ret_err
;
int
hash_algo
;
/* Receive key. */
err
=
stream_read_string
(
request
,
0
,
&
key_blob
,
&
key_blob_size
);
if
(
err
)
goto
out
;
err
=
ssh_read_key_public_from_blob
(
key_blob
,
key_blob_size
,
&
key
,
&
spec
);
if
(
err
)
goto
out
;
/* Receive data to sign. */
err
=
stream_read_string
(
request
,
0
,
&
data
,
&
data_size
);
if
(
err
)
goto
out
;
/* FIXME? */
err
=
stream_read_uint32
(
request
,
&
flags
);
if
(
err
)
goto
out
;
hash_algo
=
spec
.
hash_algo
;
if
(
!
hash_algo
)
hash_algo
=
GCRY_MD_SHA1
;
/* Use the default. */
ctrl
->
digest
.
algo
=
hash_algo
;
if
((
spec
.
flags
&
SPEC_FLAG_USE_PKCS1V2
))
ctrl
->
digest
.
raw_value
=
0
;
else
ctrl
->
digest
.
raw_value
=
1
;
/* Calculate key grip. */
err
=
ssh_key_grip
(
key
,
key_grip
);
if
(
err
)
goto
out
;
ctrl
->
have_keygrip
=
1
;
memcpy
(
ctrl
->
keygrip
,
key_grip
,
20
);
/* Hash data unless we use EdDSA. */
if
((
spec
.
flags
&
SPEC_FLAG_IS_EdDSA
))
{
ctrl
->
digest
.
valuelen
=
0
;
}
else
{
hash_n
=
gcry_md_get_algo_dlen
(
hash_algo
);
if
(
!
hash_n
)
{
err
=
gpg_error
(
GPG_ERR_INTERNAL
);
goto
out
;
}
err
=
data_hash
(
data
,
data_size
,
hash_algo
,
hash
);
if
(
err
)
goto
out
;
memcpy
(
ctrl
->
digest
.
value
,
hash
,
hash_n
);
ctrl
->
digest
.
valuelen
=
hash_n
;
}
/* Sign data. */
if
((
spec
.
flags
&
SPEC_FLAG_IS_EdDSA
))
err
=
data_sign
(
ctrl
,
&
spec
,
data
,
data_size
,
&
sig
,
&
sig_n
);
else
err
=
data_sign
(
ctrl
,
&
spec
,
NULL
,
0
,
&
sig
,
&
sig_n
);
out
:
/* Done. */
if
(
!
err
)
{
ret_err
=
stream_write_byte
(
response
,
SSH_RESPONSE_SIGN_RESPONSE
);
if
(
ret_err
)
goto
leave
;
ret_err
=
stream_write_string
(
response
,
sig
,
sig_n
);
if
(
ret_err
)
goto
leave
;
}
else
{
log_error
(
"ssh sign request failed: %s <%s>
\n
"
,
gpg_strerror
(
err
),
gpg_strsource
(
err
));
ret_err
=
stream_write_byte
(
response
,
SSH_RESPONSE_FAILURE
);
if
(
ret_err
)
goto
leave
;
}
leave
:
gcry_sexp_release
(
key
);
xfree
(
key_blob
);
xfree
(
data
);
es_free
(
sig
);
return
ret_err
;
}
/* This function extracts the comment contained in the key
s-expression KEY and stores a copy in COMMENT. Returns usual error
code. */
static
gpg_error_t
ssh_key_extract_comment
(
gcry_sexp_t
key
,
char
**
r_comment
)
{
gcry_sexp_t
comment_list
;
*
r_comment
=
NULL
;
comment_list
=
gcry_sexp_find_token
(
key
,
"comment"
,
0
);
if
(
!
comment_list
)
return
gpg_error
(
GPG_ERR_INV_SEXP
);
*
r_comment
=
gcry_sexp_nth_string
(
comment_list
,
1
);
gcry_sexp_release
(
comment_list
);
if
(
!*
r_comment
)
return
gpg_error
(
GPG_ERR_INV_SEXP
);
return
0
;
}
/* This function converts the key contained in the S-Expression KEY
into a buffer, which is protected by the passphrase PASSPHRASE.
Returns usual error code. */
static
gpg_error_t
ssh_key_to_protected_buffer
(
gcry_sexp_t
key
,
const
char
*
passphrase
,
unsigned
char
**
buffer
,
size_t
*
buffer_n
)
{
unsigned
char
*
buffer_new
;
unsigned
int
buffer_new_n
;
gpg_error_t
err
;
err
=
0
;
buffer_new_n
=
gcry_sexp_sprint
(
key
,
GCRYSEXP_FMT_CANON
,
NULL
,
0
);
buffer_new
=
xtrymalloc_secure
(
buffer_new_n
);
if
(
!
buffer_new
)
{
err
=
gpg_error_from_syserror
();
goto
out
;
}
gcry_sexp_sprint
(
key
,
GCRYSEXP_FMT_CANON
,
buffer_new
,
buffer_new_n
);
/* FIXME: guarantee? */
err
=
agent_protect
(
buffer_new
,
passphrase
,
buffer
,
buffer_n
,
0
,
-1
);
out
:
xfree
(
buffer_new
);
return
err
;
}
/* Callback function to compare the first entered PIN with the one
currently being entered. */
static
gpg_error_t
reenter_compare_cb
(
struct
pin_entry_info_s
*
pi
)
{
const
char
*
pin1
=
pi
->
check_cb_arg
;
if
(
!
strcmp
(
pin1
,
pi
->
pin
))
return
0
;
/* okay */
return
gpg_error
(
GPG_ERR_BAD_PASSPHRASE
);
}
/* Store the ssh KEY into our local key storage and protect it after
asking for a passphrase. Cache that passphrase. TTL is the
maximum caching time for that key. If the key already exists in
our key storage, don't do anything. When entering a key also add
an entry to the sshcontrol file. */
static
gpg_error_t
ssh_identity_register
(
ctrl_t
ctrl
,
ssh_key_type_spec_t
*
spec
,
gcry_sexp_t
key
,
int
ttl
,
int
confirm
)
{
gpg_error_t
err
;
unsigned
char
key_grip_raw
[
20
];
char
key_grip
[
41
];
unsigned
char
*
buffer
=
NULL
;
size_t
buffer_n
;
char
*
description
=
NULL
;
const
char
*
description2
=
L_
(
"Please re-enter this passphrase"
);
char
*
comment
=
NULL
;
char
*
key_fpr
=
NULL
;
const
char
*
initial_errtext
=
NULL
;
struct
pin_entry_info_s
*
pi
=
NULL
;
struct
pin_entry_info_s
*
pi2
=
NULL
;
err
=
ssh_key_grip
(
key
,
key_grip_raw
);
if
(
err
)
goto
out
;
bin2hex
(
key_grip_raw
,
20
,
key_grip
);
err
=
ssh_get_fingerprint_string
(
key
,
&
key_fpr
);
if
(
err
)
goto
out
;
/* Check whether the key is already in our key storage. Don't do
anything then besides (re-)adding it to sshcontrol. */
if
(
!
agent_key_available
(
key_grip_raw
)
)
goto
key_exists
;
/* Yes, key is available. */
err
=
ssh_key_extract_comment
(
key
,
&
comment
);
if
(
err
)
goto
out
;
if
(
asprintf
(
&
description
,
L_
(
"Please enter a passphrase to protect"
" the received secret key%%0A"
" %s%%0A"
" %s%%0A"
"within gpg-agent's key storage"
),
key_fpr
,
comment
?
comment
:
""
)
<
0
)
{
err
=
gpg_error_from_syserror
();
goto
out
;
}
pi
=
gcry_calloc_secure
(
1
,
sizeof
(
*
pi
)
+
MAX_PASSPHRASE_LEN
+
1
);
if
(
!
pi
)
{
err
=
gpg_error_from_syserror
();
goto
out
;
}
pi2
=
gcry_calloc_secure
(
1
,
sizeof
(
*
pi2
)
+
MAX_PASSPHRASE_LEN
+
1
);
if
(
!
pi2
)
{
err
=
gpg_error_from_syserror
();
goto
out
;
}
pi
->
max_length
=
MAX_PASSPHRASE_LEN
+
1
;
pi
->
max_tries
=
1
;
pi
->
with_repeat
=
1
;
pi2
->
max_length
=
MAX_PASSPHRASE_LEN
+
1
;
pi2
->
max_tries
=
1
;
pi2
->
check_cb
=
reenter_compare_cb
;
pi2
->
check_cb_arg
=
pi
->
pin
;
next_try
:
err
=
agent_askpin
(
ctrl
,
description
,
NULL
,
initial_errtext
,
pi
,
NULL
,
0
);
initial_errtext
=
NULL
;
if
(
err
)
goto
out
;
/* Unless the passphrase is empty or the pinentry told us that
it already did the repetition check, ask to confirm it. */
if
(
*
pi
->
pin
&&
!
pi
->
repeat_okay
)
{
err
=
agent_askpin
(
ctrl
,
description2
,
NULL
,
NULL
,
pi2
,
NULL
,
0
);
if
(
gpg_err_code
(
err
)
==
GPG_ERR_BAD_PASSPHRASE
)
{
/* The re-entered one did not match and the user did not
hit cancel. */
initial_errtext
=
L_
(
"does not match - try again"
);
goto
next_try
;
}
}
err
=
ssh_key_to_protected_buffer
(
key
,
pi
->
pin
,
&
buffer
,
&
buffer_n
);
if
(
err
)
goto
out
;
/* Store this key to our key storage. */
err
=
agent_write_private_key
(
key_grip_raw
,
buffer
,
buffer_n
,
0
);
if
(
err
)
goto
out
;
/* Cache this passphrase. */
err
=
agent_put_cache
(
key_grip
,
CACHE_MODE_SSH
,
pi
->
pin
,
ttl
);
if
(
err
)
goto
out
;
key_exists
:
/* And add an entry to the sshcontrol file. */
err
=
add_control_entry
(
ctrl
,
spec
,
key_grip
,
key_fpr
,
ttl
,
confirm
);
out
:
if
(
pi2
&&
pi2
->
max_length
)
wipememory
(
pi2
->
pin
,
pi2
->
max_length
);
xfree
(
pi2
);
if
(
pi
&&
pi
->
max_length
)
wipememory
(
pi
->
pin
,
pi
->
max_length
);
xfree
(
pi
);
xfree
(
buffer
);
xfree
(
comment
);
xfree
(
key_fpr
);
xfree
(
description
);
return
err
;
}
/* This function removes the key contained in the S-Expression KEY
from the local key storage, in case it exists there. Returns usual
error code. FIXME: this function is a stub. */
static
gpg_error_t
ssh_identity_drop
(
gcry_sexp_t
key
)
{
unsigned
char
key_grip
[
21
]
=
{
0
};
gpg_error_t
err
;
err
=
ssh_key_grip
(
key
,
key_grip
);
if
(
err
)
goto
out
;
key_grip
[
sizeof
(
key_grip
)
-
1
]
=
0
;
/* FIXME: What to do here - forgetting the passphrase or deleting
the key from key cache? */
out
:
return
err
;
}
/* Handler for the "add_identity" command. */
static
gpg_error_t
ssh_handler_add_identity
(
ctrl_t
ctrl
,
estream_t
request
,
estream_t
response
)
{
gpg_error_t
ret_err
;
ssh_key_type_spec_t
spec
;
gpg_error_t
err
;
gcry_sexp_t
key
;
unsigned
char
b
;
int
confirm
;
int
ttl
;
confirm
=
0
;
key
=
NULL
;
ttl
=
0
;
/* FIXME? */
err
=
ssh_receive_key
(
request
,
&
key
,
1
,
1
,
&
spec
);
if
(
err
)
goto
out
;
while
(
1
)
{
err
=
stream_read_byte
(
request
,
&
b
);
if
(
gpg_err_code
(
err
)
==
GPG_ERR_EOF
)
{
err
=
0
;
break
;
}
switch
(
b
)
{
case
SSH_OPT_CONSTRAIN_LIFETIME
:
{
u32
n
=
0
;
err
=
stream_read_uint32
(
request
,
&
n
);
if
(
!
err
)
ttl
=
n
;
break
;
}
case
SSH_OPT_CONSTRAIN_CONFIRM
:
{
confirm
=
1
;
break
;
}
default
:
/* FIXME: log/bad? */
break
;
}
}
if
(
err
)
goto
out
;
err
=
ssh_identity_register
(
ctrl
,
&
spec
,
key
,
ttl
,
confirm
);
out
:
gcry_sexp_release
(
key
);
if
(
!
err
)
ret_err
=
stream_write_byte
(
response
,
SSH_RESPONSE_SUCCESS
);
else
ret_err
=
stream_write_byte
(
response
,
SSH_RESPONSE_FAILURE
);
return
ret_err
;
}
/* Handler for the "remove_identity" command. */
static
gpg_error_t
ssh_handler_remove_identity
(
ctrl_t
ctrl
,
estream_t
request
,
estream_t
response
)
{
unsigned
char
*
key_blob
;
u32
key_blob_size
;
gcry_sexp_t
key
;
gpg_error_t
ret_err
;
gpg_error_t
err
;
(
void
)
ctrl
;
/* Receive key. */
key_blob
=
NULL
;
key
=
NULL
;
err
=
stream_read_string
(
request
,
0
,
&
key_blob
,
&
key_blob_size
);
if
(
err
)
goto
out
;
err
=
ssh_read_key_public_from_blob
(
key_blob
,
key_blob_size
,
&
key
,
NULL
);
if
(
err
)
goto
out
;
err
=
ssh_identity_drop
(
key
);
out
:
xfree
(
key_blob
);
gcry_sexp_release
(
key
);
if
(
!
err
)
ret_err
=
stream_write_byte
(
response
,
SSH_RESPONSE_SUCCESS
);
else
ret_err
=
stream_write_byte
(
response
,
SSH_RESPONSE_FAILURE
);
return
ret_err
;
}
/* FIXME: stub function. Actually useful? */
static
gpg_error_t
ssh_identities_remove_all
(
void
)
{
gpg_error_t
err
;
err
=
0
;
/* FIXME: shall we remove _all_ cache entries or only those
registered through the ssh-agent protocol? */
return
err
;
}
/* Handler for the "remove_all_identities" command. */
static
gpg_error_t
ssh_handler_remove_all_identities
(
ctrl_t
ctrl
,
estream_t
request
,
estream_t
response
)
{
gpg_error_t
ret_err
;
gpg_error_t
err
;
(
void
)
ctrl
;
(
void
)
request
;
err
=
ssh_identities_remove_all
();
if
(
!
err
)
ret_err
=
stream_write_byte
(
response
,
SSH_RESPONSE_SUCCESS
);
else
ret_err
=
stream_write_byte
(
response
,
SSH_RESPONSE_FAILURE
);
return
ret_err
;
}
/* Lock agent? FIXME: stub function. */
static
gpg_error_t
ssh_lock
(
void
)
{
gpg_error_t
err
;
/* FIXME */
log_error
(
"ssh-agent's lock command is not implemented
\n
"
);
err
=
0
;
return
err
;
}
/* Unock agent? FIXME: stub function. */
static
gpg_error_t
ssh_unlock
(
void
)
{
gpg_error_t
err
;
log_error
(
"ssh-agent's unlock command is not implemented
\n
"
);
err
=
0
;
return
err
;
}
/* Handler for the "lock" command. */
static
gpg_error_t
ssh_handler_lock
(
ctrl_t
ctrl
,
estream_t
request
,
estream_t
response
)
{
gpg_error_t
ret_err
;
gpg_error_t
err
;
(
void
)
ctrl
;
(
void
)
request
;
err
=
ssh_lock
();
if
(
!
err
)
ret_err
=
stream_write_byte
(
response
,
SSH_RESPONSE_SUCCESS
);
else
ret_err
=
stream_write_byte
(
response
,
SSH_RESPONSE_FAILURE
);
return
ret_err
;
}
/* Handler for the "unlock" command. */
static
gpg_error_t
ssh_handler_unlock
(
ctrl_t
ctrl
,
estream_t
request
,
estream_t
response
)
{
gpg_error_t
ret_err
;
gpg_error_t
err
;
(
void
)
ctrl
;
(
void
)
request
;
err
=
ssh_unlock
();
if
(
!
err
)
ret_err
=
stream_write_byte
(
response
,
SSH_RESPONSE_SUCCESS
);
else
ret_err
=
stream_write_byte
(
response
,
SSH_RESPONSE_FAILURE
);
return
ret_err
;
}
/* Return the request specification for the request identified by TYPE
or NULL in case the requested request specification could not be
found. */
static
ssh_request_spec_t
*
request_spec_lookup
(
int
type
)
{
ssh_request_spec_t
*
spec
;
unsigned
int
i
;
for
(
i
=
0
;
i
<
DIM
(
request_specs
);
i
++
)
if
(
request_specs
[
i
].
type
==
type
)
break
;
if
(
i
==
DIM
(
request_specs
))
{
if
(
opt
.
verbose
)
log_info
(
"ssh request %u is not supported
\n
"
,
type
);
spec
=
NULL
;
}
else
spec
=
request_specs
+
i
;
return
spec
;
}
/* Process a single request. The request is read from and the
response is written to STREAM_SOCK. Uses CTRL as context. Returns
zero in case of success, non zero in case of failure. */
static
int
ssh_request_process
(
ctrl_t
ctrl
,
estream_t
stream_sock
)
{
ssh_request_spec_t
*
spec
;
estream_t
response
=
NULL
;
estream_t
request
=
NULL
;
unsigned
char
request_type
;
gpg_error_t
err
;
int
send_err
=
0
;
int
ret
;
unsigned
char
*
request_data
=
NULL
;
u32
request_data_size
;
u32
response_size
;
/* Create memory streams for request/response data. The entire
request will be stored in secure memory, since it might contain
secret key material. The response does not have to be stored in
secure memory, since we never give out secret keys.
Note: we only have little secure memory, but there is NO
possibility of DoS here; only trusted clients are allowed to
connect to the agent. What could happen is that the agent
returns out-of-secure-memory errors on requests in case the
agent's owner floods his own agent with many large messages.
-moritz */
/* Retrieve request. */
err
=
stream_read_string
(
stream_sock
,
1
,
&
request_data
,
&
request_data_size
);
if
(
err
)
goto
out
;
if
(
opt
.
verbose
>
1
)
log_info
(
"received ssh request of length %u
\n
"
,
(
unsigned
int
)
request_data_size
);
if
(
!
request_data_size
)
{
send_err
=
1
;
goto
out
;
/* Broken request; FIXME. */
}
request_type
=
request_data
[
0
];
spec
=
request_spec_lookup
(
request_type
);
if
(
!
spec
)
{
send_err
=
1
;
goto
out
;
/* Unknown request; FIXME. */
}
if
(
spec
->
secret_input
)
request
=
es_mopen
(
NULL
,
0
,
0
,
1
,
realloc_secure
,
gcry_free
,
"r+b"
);
else
request
=
es_mopen
(
NULL
,
0
,
0
,
1
,
gcry_realloc
,
gcry_free
,
"r+b"
);
if
(
!
request
)
{
err
=
gpg_error_from_syserror
();
goto
out
;
}
ret
=
es_setvbuf
(
request
,
NULL
,
_IONBF
,
0
);
if
(
ret
)
{
err
=
gpg_error_from_syserror
();
goto
out
;
}
err
=
stream_write_data
(
request
,
request_data
+
1
,
request_data_size
-
1
);
if
(
err
)
goto
out
;
es_rewind
(
request
);
response
=
es_fopenmem
(
0
,
"r+b"
);
if
(
!
response
)
{
err
=
gpg_error_from_syserror
();
goto
out
;
}
if
(
opt
.
verbose
)
log_info
(
"ssh request handler for %s (%u) started
\n
"
,
spec
->
identifier
,
spec
->
type
);
err
=
(
*
spec
->
handler
)
(
ctrl
,
request
,
response
);
if
(
opt
.
verbose
)
{
if
(
err
)
log_info
(
"ssh request handler for %s (%u) failed: %s
\n
"
,
spec
->
identifier
,
spec
->
type
,
gpg_strerror
(
err
));
else
log_info
(
"ssh request handler for %s (%u)
ready
\
n
",
spec
->
identifier
,
spec
->
type
);
}
if
(
err
)
{
send_err
=
1
;
goto
out
;
}
response_size
=
es_ftell
(
response
);
if
(
opt
.
verbose
>
1
)
log_info
(
"sending ssh response of length %u
\n
"
,
(
unsigned
int
)
response_size
);
err
=
es_fseek
(
response
,
0
,
SEEK_SET
);
if
(
err
)
{
send_err
=
1
;
goto
out
;
}
err
=
stream_write_uint32
(
stream_sock
,
response_size
);
if
(
err
)
{
send_err
=
1
;
goto
out
;
}
err
=
stream_copy
(
stream_sock
,
response
);
if
(
err
)
goto
out
;
err
=
es_fflush
(
stream_sock
);
if
(
err
)
goto
out
;
out
:
if
(
err
&&
es_feof
(
stream_sock
))
log_error
(
"error occurred while processing request: %s
\n
"
,
gpg_strerror
(
err
));
if
(
send_err
)
{
if
(
opt
.
verbose
>
1
)
log_info
(
"sending ssh error response
\n
"
);
err
=
stream_write_uint32
(
stream_sock
,
1
);
if
(
err
)
goto
leave
;
err
=
stream_write_byte
(
stream_sock
,
SSH_RESPONSE_FAILURE
);
if
(
err
)
goto
leave
;
}
leave
:
es_fclose
(
request
);
es_fclose
(
response
);
xfree
(
request_data
);
return
!!
err
;
}
/* Return the peer's pid. */
static
unsigned
long
get_client_pid
(
int
fd
)
{
pid_t
client_pid
=
(
pid_t
)(
-1
);
#ifdef SO_PEERCRED
{
#ifdef HAVE_STRUCT_SOCKPEERCRED_PID
struct
sockpeercred
cr
;
#else
struct
ucred
cr
;
#endif
socklen_t
cl
=
sizeof
cr
;
if
(
!
getsockopt
(
fd
,
SOL_SOCKET
,
SO_PEERCRED
,
&
cr
,
&
cl
))
{
#if defined (HAVE_STRUCT_SOCKPEERCRED_PID) || defined (HAVE_STRUCT_UCRED_PID)
client_pid
=
cr
.
pid
;
#elif defined (HAVE_STRUCT_UCRED_CR_PID)
client_pid
=
cr
.
cr_pid
;
#else
#error "Unknown SO_PEERCRED struct"
#endif
}
}
#elif defined (LOCAL_PEERPID)
{
socklen_t
len
=
sizeof
(
pid_t
);
getsockopt
(
fd
,
SOL_LOCAL
,
LOCAL_PEERPID
,
&
client_pid
,
&
len
);
}
#elif defined (LOCAL_PEEREID)
{
struct
unpcbid
unp
;
socklen_t
unpl
=
sizeof
unp
;
if
(
getsockopt
(
fd
,
0
,
LOCAL_PEEREID
,
&
unp
,
&
unpl
)
!=
-1
)
client_pid
=
unp
.
unp_pid
;
}
#elif defined (HAVE_GETPEERUCRED)
{
ucred_t
*
ucred
=
NULL
;
if
(
getpeerucred
(
fd
,
&
ucred
)
!=
-1
)
{
client_pid
=
ucred_getpid
(
ucred
);
ucred_free
(
ucred
);
}
}
#endif
return
client_pid
==
(
pid_t
)(
-1
)
?
0
:
(
unsigned
long
)
client_pid
;
}
/* Start serving client on SOCK_CLIENT. */
void
start_command_handler_ssh
(
ctrl_t
ctrl
,
gnupg_fd_t
sock_client
)
{
estream_t
stream_sock
=
NULL
;
gpg_error_t
err
;
int
ret
;
err
=
agent_copy_startup_env
(
ctrl
);
if
(
err
)
goto
out
;
ctrl
->
client_pid
=
get_client_pid
(
FD2INT
(
sock_client
));
/* Create stream from socket. */
stream_sock
=
es_fdopen
(
FD2INT
(
sock_client
),
"r+"
);
if
(
!
stream_sock
)
{
err
=
gpg_error_from_syserror
();
log_error
(
_
(
"failed to create stream from socket: %s
\n
"
),
gpg_strerror
(
err
));
goto
out
;
}
/* We have to disable the estream buffering, because the estream
core doesn't know about secure memory. */
ret
=
es_setvbuf
(
stream_sock
,
NULL
,
_IONBF
,
0
);
if
(
ret
)
{
err
=
gpg_error_from_syserror
();
log_error
(
"failed to disable buffering "
"on socket stream: %s
\n
"
,
gpg_strerror
(
err
));
goto
out
;
}
/* Main processing loop. */
while
(
!
ssh_request_process
(
ctrl
,
stream_sock
)
)
{
/* Check wether we have reached EOF before trying to read
another request. */
int
c
;
c
=
es_fgetc
(
stream_sock
);
if
(
c
==
EOF
)
break
;
es_ungetc
(
c
,
stream_sock
);
}
/* Reset the SCD in case it has been used. */
agent_reset_scd
(
ctrl
);
out
:
if
(
stream_sock
)
es_fclose
(
stream_sock
);
}
#ifdef HAVE_W32_SYSTEM
/* Serve one ssh-agent request. This is used for the Putty support.
REQUEST is the mmapped memory which may be accessed up to a
length of MAXREQLEN. Returns 0 on success which also indicates
that a valid SSH response message is now in REQUEST. */
int
serve_mmapped_ssh_request
(
ctrl_t
ctrl
,
unsigned
char
*
request
,
size_t
maxreqlen
)
{
gpg_error_t
err
;
int
send_err
=
0
;
int
valid_response
=
0
;
ssh_request_spec_t
*
spec
;
u32
msglen
;
estream_t
request_stream
,
response_stream
;
if
(
agent_copy_startup_env
(
ctrl
))
goto
leave
;
/* Error setting up the environment. */
if
(
maxreqlen
<
5
)
goto
leave
;
/* Caller error. */
msglen
=
uint32_construct
(
request
[
0
],
request
[
1
],
request
[
2
],
request
[
3
]);
if
(
msglen
<
1
||
msglen
>
maxreqlen
-
4
)
{
log_error
(
"ssh message len (%u) out of range"
,
(
unsigned
int
)
msglen
);
goto
leave
;
}
spec
=
request_spec_lookup
(
request
[
4
]);
if
(
!
spec
)
{
send_err
=
1
;
/* Unknown request type. */
goto
leave
;
}
/* Create a stream object with the data part of the request. */
if
(
spec
->
secret_input
)
request_stream
=
es_mopen
(
NULL
,
0
,
0
,
1
,
realloc_secure
,
gcry_free
,
"r+"
);
else
request_stream
=
es_mopen
(
NULL
,
0
,
0
,
1
,
gcry_realloc
,
gcry_free
,
"r+"
);
if
(
!
request_stream
)
{
err
=
gpg_error_from_syserror
();
goto
leave
;
}
/* We have to disable the estream buffering, because the estream
core doesn't know about secure memory. */
if
(
es_setvbuf
(
request_stream
,
NULL
,
_IONBF
,
0
))
{
err
=
gpg_error_from_syserror
();
goto
leave
;
}
/* Copy the request to the stream but omit the request type. */
err
=
stream_write_data
(
request_stream
,
request
+
5
,
msglen
-
1
);
if
(
err
)
goto
leave
;
es_rewind
(
request_stream
);
response_stream
=
es_fopenmem
(
0
,
"r+b"
);
if
(
!
response_stream
)
{
err
=
gpg_error_from_syserror
();
goto
leave
;
}
if
(
opt
.
verbose
)
log_info
(
"ssh request handler for %s (%u) started
\n
"
,
spec
->
identifier
,
spec
->
type
);
err
=
(
*
spec
->
handler
)
(
ctrl
,
request_stream
,
response_stream
);
if
(
opt
.
verbose
)
{
if
(
err
)
log_info
(
"ssh request handler for %s (%u) failed: %s
\n
"
,
spec
->
identifier
,
spec
->
type
,
gpg_strerror
(
err
));
else
log_info
(
"ssh request handler for %s (%u)
ready
\
n
",
spec
->
identifier
,
spec
->
type
);
}
es_fclose
(
request_stream
);
request_stream
=
NULL
;
if
(
err
)
{
send_err
=
1
;
goto
leave
;
}
/* Put the response back into the mmapped buffer. */
{
void
*
response_data
;
size_t
response_size
;
/* NB: In contrast to the request-stream, the response stream
includes the message type byte. */
if
(
es_fclose_snatch
(
response_stream
,
&
response_data
,
&
response_size
))
{
log_error
(
"snatching ssh response failed: %s"
,
gpg_strerror
(
gpg_error_from_syserror
()));
send_err
=
1
;
/* Ooops. */
goto
leave
;
}
if
(
opt
.
verbose
>
1
)
log_info
(
"sending ssh response of length %u
\n
"
,
(
unsigned
int
)
response_size
);
if
(
response_size
>
maxreqlen
-
4
)
{
log_error
(
"invalid length of the ssh response: %s"
,
gpg_strerror
(
GPG_ERR_INTERNAL
));
es_free
(
response_data
);
send_err
=
1
;
goto
leave
;
}
request
[
0
]
=
response_size
>>
24
;
request
[
1
]
=
response_size
>>
16
;
request
[
2
]
=
response_size
>>
8
;
request
[
3
]
=
response_size
>>
0
;
memcpy
(
request
+
4
,
response_data
,
response_size
);
es_free
(
response_data
);
valid_response
=
1
;
}
leave
:
if
(
send_err
)
{
request
[
0
]
=
0
;
request
[
1
]
=
0
;
request
[
2
]
=
0
;
request
[
3
]
=
1
;
request
[
4
]
=
SSH_RESPONSE_FAILURE
;
valid_response
=
1
;
}
/* Reset the SCD in case it has been used. */
agent_reset_scd
(
ctrl
);
return
valid_response
?
0
:
-1
;
}
#endif
/*HAVE_W32_SYSTEM*/
File Metadata
Details
Attached
Mime Type
text/x-c
Expires
Tue, Jun 3, 5:40 AM (10 h, 42 m)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
36/18/3e66787b2779a203629d39d4cb32
Attached To
rG GnuPG
Event Timeline
Log In to Comment